Compare commits

..

30 Commits

Author SHA1 Message Date
fdardenne
c6105bcf24 [IMP] create JavaScript howtos
The JavaScript cheatsheet is outdated, we therefore remove it and
replace it by multiple howtos:

- Create a view from scratch
- Extending an existing view
- Create a field from scratch
- Extend an existing field
- Create a client action

There is other subjects to introduce as the web framework is big. Other
future contributions will cover them.

X-original-commit: 7e4435deb8
2023-04-04 17:16:15 +02:00
oskarenablebanking
c91ea3d251 [ADD] accounting: enablebanking
task-3232701

closes odoo/documentation#4001

X-original-commit: cc194eb979
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
2023-04-03 20:02:15 +02:00
Jonathan Castillo (jcs)
9878b58a04 [MOV] accounting: structure of bank sync docs
X-original-commit: d694c6c9f5
Part-of: odoo/documentation#4001
2023-04-03 20:02:15 +02:00
Zuzanna Luczynska
fdea740edc [ADD] field service: default warehouse
task-2948598

closes odoo/documentation#3998

X-original-commit: 40ef1a26f9
Signed-off-by: zulu-odoo <zulu@odoo.com>
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
2023-04-03 18:26:24 +02:00
Benoit Socias
a8deaf9594 [IMP] tutorials: add warning about mutable global variables
The whole concept of multi-tenancy is not really approached within the
tutorial.

This commit adds a warning about never using mutable global variables
within odoo to seed the idea in the reader's mind.

task-3059110

closes odoo/documentation#3990

X-original-commit: 1ce7166f49
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Signed-off-by: Benoit Socias (bso) <bso@odoo.com>
2023-04-03 15:01:46 +02:00
“Audrey
4914c45d88 [IMP] Website: review website translation page
task-3255779

closes odoo/documentation#3977

X-original-commit: 67bada80e6
Signed-off-by: auva-odoo <auva@odoo.com>
2023-04-03 10:53:02 +02:00
Felicious
6757b2691b [IMP]: add manual valuation section
Add images and manual valuation

add ref tag and retitle doc to sentence case

Remove trailing whitespaces

update explanation

closes odoo/documentation#3983

X-original-commit: 71ede7a3d4
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-04-01 04:55:39 +02:00
Tom Aarab (toaa)
11b2c5250b [IMP] Adyen: additional minimum requirements for users
Adding requirements for users to use Adyen. Forward to master.

closes odoo/documentation#3962

Taskid: 3159712
X-original-commit: 9b0a54b7f2
Signed-off-by: Aarab Tom (toaa) <toaa@odoo.com>
2023-03-30 08:37:51 +02:00
Loan (lse)
d6acaf70c1 [IMP] ePoS: vulgarise the SSL ePos issue
SSL/HTTPS topic is complicated for most of
Odoo customers as it is quite technical.

This PR should help them guide them to better
understand the issue and how to fix it themselves.

Support can't be provided to each device, browsers and OS.
But we did add some guides regarding the more
"popular" ones and some "keyword" to search
online for the others.

closes odoo/documentation#3924

X-original-commit: 415a817c57
Signed-off-by: Platteau Xavier (xpl) <xpl@odoo.com>
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
Co-authored-by: Loredana Perazzo <lrpz@odoo.com>
2023-03-29 22:53:50 +02:00
Xavier
ec40c817da [ADD] attendances: hr and attendances categories + hardware page
Task ID: 3251124

closes odoo/documentation#3959

X-original-commit: 0124878dd4
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
2023-03-29 21:45:52 +02:00
Loredana Perazzo
69248346db [IMP] pos: update fiscal positions page
Task ID: 2862506

closes odoo/documentation#3927

X-original-commit: 4a7acf8c02
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
2023-03-29 08:32:22 +02:00
Melanie Nguyen (meng)
c69cb37be3 [IMP] mail plugins: add instructions to gmail plugin
closes odoo/documentation#3946

X-original-commit: 404d524deb
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-03-29 04:39:48 +02:00
Melanie Nguyen (meng)
19288088f9 [IMP] sales: menuselection fix
Fixed a menuselection error and deleted instances of second-person pov
Closes task 3116083

closes odoo/documentation#3941

X-original-commit: f30f6d2003
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-03-29 03:40:34 +02:00
Timothy Kukulka (tiku)
24f4348a46 [IMP] General: Oauth seemore additions
closes odoo/documentation#3936

X-original-commit: aa4e5d7a01
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-03-29 03:40:27 +02:00
Antoine Vandevenne (anv)
dc2a988173 [IMP] supported_version: release saas-16.2
closes odoo/documentation#3908

X-original-commit: c9f53c5b88
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
2023-03-28 17:09:42 +02:00
Felicious
9fb4c01b19 [FIX] inventory: fix BoM typo
closes odoo/documentation#3912

X-original-commit: f2010faafc
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-03-28 01:05:34 +02:00
Nicolas (vin)
7effc12f02 [IMP] coding_guidelines: fix python code indents
A few python code blocks on the coding guidelines are indented twice
(8 spaces instead of 4), which is not correct.

closes odoo/documentation#3905

X-original-commit: 16176fb508
Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
2023-03-27 18:37:39 +02:00
“Chiara
0d6466fbbf [IMP] accounting: bank transactions
task-3204835

closes odoo/documentation#3896

X-original-commit: e89211e451
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
2023-03-27 15:20:00 +02:00
Jess Rogers (jero)
9651908b73 [IMP] helpdesk: updated ticketing channels setup
closes odoo/documentation#3879

X-original-commit: ab0d9c239d
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-03-26 10:09:56 +02:00
Timothy Kukulka (tiku)
5c89a56634 [IMP] Support: Update What can I expect
closes odoo/documentation#3885

X-original-commit: 406e12822e
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-03-26 09:12:04 +02:00
LoredanaLrpz
7c4efb6fa9 [IMP] pos: and update kitchen printing
Task ID: 3235139

closes odoo/documentation#3874

X-original-commit: e6fd7db226
Signed-off-by: Platteau Xavier (xpl) <xpl@odoo.com>
Signed-off-by: Perazzo Loredana (lrpz) <lrpz@odoo.com>
2023-03-25 01:02:02 +01:00
Jess Rogers (jero)
7c8b8269eb [IMP] helpdesk: updated teams and stages setup content
closes odoo/documentation#3869

X-original-commit: da2e4a1a60
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Signed-off-by: Zachary Straub (zst) <zst@odoo.com>
2023-03-25 01:01:42 +01:00
Brandon Seltenrich (BRSE)
ccd282023a [IMP] inventory: fix sendcloud doc
closes odoo/documentation#3870

X-original-commit: b9453515a0
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Signed-off-by: Brandon Seltenrich (brse) <brse@odoo.com>
2023-03-24 18:07:45 +01:00
Olivier Dony
19b4797d37 [FIX] legal: fix some broken links (pdfs, translations)
1) PDF files are generated and stored at the root of the CURRENT_BRANCH
directory. The links to those files are generated at different levels of
the doctree, which makes it impossible to use a relative path.
For example the same "Enterprise Agreement" doc in EN is published on:
 - /16.0/legal/terms/enterprise.html
 - /16.0/fr/legal/terms/enterprise.html

As a workaround, use absolute links for the PDFs. They won't work
locally for now. Can be improved later, as long as we don't break
those links located in various depths of the troctree.

2) The legal constracts aren't translated in all availables languages
(yet), so those links are 404s now. Introduced a conf.py variable
`legal_translations` with the list of languages where translated
contracts are indeed available, and falling back to the EN version
otherwise. Some languages don't have *all* the contracts translated, so
some 404 may remain temporarily.

Forward-port of f69dba70be
2023-03-23 16:42:57 +01:00
Tom Aarab (toaa)
6def5713d3 [ADD] eCommerce: customer interaction
Adding a page on customer interaction and adding redirects. Forward to
master.

closes odoo/documentation#3861

Taskid: 3224716
X-original-commit: c24f6eca7a
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Signed-off-by: Aarab Tom (toaa) <toaa@odoo.com>
2023-03-21 16:34:55 +01:00
LoredanaLrpz
cc997535b2 [IMP] pos: take the half-up out of the last note
Task ID: 3184227

closes odoo/documentation#3856

X-original-commit: f3aafe2f09
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
2023-03-21 11:58:08 +01:00
Valentin Vallaeys (vava)
cb06fb7687 [ADD] developer: add _get_available_tokens
Introduced with this commit:
e534ab41e7

closes odoo/documentation#3798

Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
2023-03-21 11:57:57 +01:00
Jonathan Castillo (jcs)
a18fe9a25b [FIX] dev/tuto: grammar mistake in 02_setup
task-3238089

closes odoo/documentation#3848

X-original-commit: 6866cfa361
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Signed-off-by: Castillo Jonathan (jcs) <jcs@odoo.com>
2023-03-20 16:00:35 +01:00
Antoine Vandevenne (anv)
3f0a7cd34e [IMP] supported_versions: flag saas-16.1 as supported
closes odoo/documentation#3840

X-original-commit: c466d6fd6e
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
2023-03-17 12:19:02 +01:00
Christophe Monniez
aeed9418b4 [REL] saas-16.2 2023-03-15 12:18:53 +01:00
13 changed files with 519 additions and 656 deletions

View File

@@ -26,7 +26,7 @@ SOURCE_DIR = content
HTML_BUILD_DIR = $(BUILD_DIR)/html
ifdef VERSIONS
HTML_BUILD_DIR := $(HTML_BUILD_DIR)/master
HTML_BUILD_DIR := $(HTML_BUILD_DIR)/saas-16.2
endif
ifneq ($(CURRENT_LANG),en)
HTML_BUILD_DIR := $(HTML_BUILD_DIR)/$(CURRENT_LANG)

View File

@@ -22,7 +22,7 @@ copyright = 'Odoo S.A.'
# `version` is the version info for the project being documented, acts as replacement for |version|,
# also used in various other places throughout the built documents.
# `release` is the full version, including alpha/beta/rc tags. Acts as replacement for |release|.
version = release = 'master'
version = release = 'saas-16.2'
# `current_branch` is the technical name of the current branch.
# E.g., saas-15.4 -> saas-15.4; 12.0 -> 12.0, master -> master (*).

View File

@@ -9,6 +9,9 @@ How-to guides
:titlesonly:
howtos/scss_tips
howtos/javascript_field
howtos/javascript_view
howtos/javascript_client_action
howtos/web_services
howtos/company
howtos/accounting_localization
@@ -23,6 +26,21 @@ How-to guides
Follow this guide to keep the technical debt of your CSS code under control.
.. card:: Customize a field
:target: howtos/javascript_field
Learn how to customize field components in the Odoo JavaScript web framework.
.. card:: Customize a view type
:target: howtos/javascript_view
Learn how to customize view types in the Odoo JavaScript web framework.
.. card:: Create a client action
:target: howtos/javascript_client_action
Learn how to create client actions in the Odoo JavaScript web framework.
.. card:: Web services
:target: howtos/web_services

View File

@@ -0,0 +1,46 @@
======================
Create a client action
======================
A client action triggers an action that is entirely implemented in the client side.
One of the benefits of using a client action is the ability to create highly customized interfaces
with ease. A client action is typically defined by an OWL component; we can also use the web
framework and use services, core components, hooks,...
#. Create the :ref:`client action <reference/actions/client>`, don't forget to
make it accessible.
.. code-block:: xml
<record model="ir.actions.client" id="my_client_action">
<field name="name">My Client Action</field>
<field name="tag">my_module.MyClientAction</field>
</record>
#. Create a component that represents the client action.
.. code-block:: js
:caption: :file:`my_client_action.js`
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
class MyClientAction extends Component {}
MyClientAction.template = "my_module.clientaction";
// remember the tag name we put in the first step
registry.category("actions").add("my_module.MyClientAction", MyClientAction);
.. code-block:: xml
:caption: :file:`my_client_action.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_tshirt.clientaction" owl="1">
Hello world
</t>
</templates>

View File

@@ -0,0 +1,107 @@
=================
Customize a field
=================
Subclass an existing field component
====================================
Let's take an example where we want to extends the `BooleanField` to create a boolean field
displaying "Late!" in red whenever the checkbox is checked.
#. Create a new widget component extending the desired field component.
.. code-block:: javascript
:caption: :file:`late_order_boolean_field.js`
/** @odoo-module */
import { registry } from "@web/core/registry";
import { BooleanField } from "@web/views/fields/boolean/boolean_field";
import { Component, xml } from "@odoo/owl";
class LateOrderBooleanField extends BooleanField {}
LateOrderBooleanField.template = "my_module.LateOrderBooleanField";
#. Create the field template.
The component uses a new template with the name `my_module.LateOrderBooleanField`. Create it by
inheriting the current template of the `BooleanField`.
.. code-block:: xml
:caption: :file:`late_order_boolean_field.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="my_module.LateOrderBooleanField" t-inherit="web.BooleanField" owl="1">
<xpath expr="//CheckBox" position="after">
<span t-if="props.value" class="text-danger"> Late! </span>
</xpath>
</t>
</templates>
#. Register the component to the fields registry.
.. code-block::
:caption: :file:`late_order_boolean_field.js`
registry.category("fields").add("late_boolean", LateOrderBooleanField);
#. Add the widget in the view arch as an attribute of the field.
.. code-block:: xml
<field name="somefield" widget="late_boolean"/>
Create a new field component
============================
Assume that we want to create a field that displays a simple text in red.
#. Create a new Owl component representing our new field
.. code-block:: js
:caption: :file:`my_text_field.js`
/** @odoo-module */
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
export class MyTextField extends Component {
/**
* @param {boolean} newValue
*/
onChange(newValue) {
this.props.update(newValue);
}
}
MyTextField.template = xml`
<input t-att-id="props.id" class="text-danger" t-att-value="props.value" onChange.bind="onChange" />
`;
MyTextField.props = {
...standardFieldProps,
};
MyTextField.supportedTypes = ["char"];
The imported `standardFieldProps` contains the standard props passed by the `View` such as
the `update` function to update the value, the `type` of the field in the model, the
`readonly` boolean, and others.
#. In the same file, register the component to the fields registry.
.. code-block:: js
:caption: :file:`my_text_field.js`
registry.category("fields").add("my_text_field", MyTextField);
This maps the widget name in the arch to its actual component.
#. Add the widget in the view arch as an attribute of the field.
.. code-block:: xml
<field name="somefield" widget="my_text_field"/>

View File

@@ -0,0 +1,262 @@
=====================
Customize a view type
=====================
Subclass an existing view
=========================
Assume we need to create a custom version of a generic view. For example, a kanban view with some
extra ribbon-like widget on top (to display some specific custom information). In that case, this
can be done in a few steps:
#. Extend the kanban controller/renderer/model and register it in the view registry.
.. code-block:: js
:caption: :file:`custom_kanban_controller.js`
/** @odoo-module */
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { registry } from "@web/core/registry";
// the controller usually contains the Layout and the renderer.
class CustomKanbanController extends KanbanController {
// Your logic here, override or insert new methods...
// if you override setup(), don't forget to call super.setup()
}
CustomKanbanController.template = "my_module.CustomKanbanView";
export const customKanbanView = {
...kanbanView, // contains the default Renderer/Controller/Model
Controller: CustomKanbanController,
};
// Register it to the views registry
registry.category("views").add("custom_kanban", customeKanbanView);
In our custom kanban, we defined a new template. We can either inherit the kanban controller
template and add our template pieces or we can define a completely new template.
.. code-block:: xml
:caption: :file:`custom_kanban_controller.xml`
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="my_module.CustomKanbanView" t-inherit="web.KanbanView" owl="1">
<xpath expr="//Layout" position="before">
<div>
Hello world !
</div>
</xpath>
</t>
</templates>
#. Use the view with the `js_class` attribute in arch.
.. code-block:: xml
<kanban js_class="custom_kanban">
<templates>
<t t-name="kanban-box">
<!--Your comment-->
</t>
</templates>
</kanban>
The possibilities for extending views are endless. While we have only extended the controller
here, you can also extend the renderer to add new buttons, modify how records are presented, or
customize the dropdown, as well as extend other components such as the model and `buttonTemplate`.
Create a new view from scratch
==============================
Creating a new view is an advanced topic. This guide highlight only the essential steps.
#. Create the controller.
The primary role of a controller is to facilitate the coordination between various components
of a view, such as the Renderer, Model, and Layout.
.. code-block:: js
:caption: :file:`beautiful_controller.js`
/** @odoo-module */
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart, useState} from "@odoo/owl";
export class BeautifulController extends Component {
setup() {
this.orm = useService("orm");
// The controller create the model and make it reactive so whenever this.model is
// accessed and edited then it'll cause a rerendering
this.model = useState(
new this.props.Model(
this.orm,
this.props.resModel,
this.props.fields,
this.props.archInfo,
this.props.domain
)
);
onWillStart(async () => {
await this.model.load();
});
}
}
BeautifulController.template = "my_module.View";
BeautifulController.components = { Layout };
The template of the Controller displays the control panel with Layout and also the
renderer.
.. code-block:: xml
:caption: :file:`beautiful_controller.xml`
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.View" owl="1">
<Layout display="props.display" className="'h-100 overflow-auto'">
<t t-component="props.Renderer" records="model.records" propsYouWant="'Hello world'"/>
</Layout>
</t>
</templates>
#. Create the renderer.
The primary function of a renderer is to generate a visual representation of data by rendering
the view that includes records.
.. code-block:: js
:caption: :file:`beautiful_renderer.js`
import { Component } from "@odoo/owl";
export class BeautifulRenderer extends Component {}
BeautifulRenderer.template = "my_module.Renderer";
.. code-block:: xml
:caption: :file:`beautiful_renderer.xml`
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.Renderer" owl="1">
<t t-esc="props.propsYouWant"/>
<t t-foreach="props.records" t-as="record" t-key="record.id">
// Show records
</t>
</t>
</templates>
#. Create the model.
The role of the model is to retrieve and manage all the necessary data in the view.
.. code-block:: js
:caption: :file:`beautiful_model.js`
/** @odoo-module */
import { KeepLast } from "@web/core/utils/concurrency";
export class BeautifulModel {
constructor(orm, resModel, fields, archInfo, domain) {
this.orm = orm;
this.resModel = resModel;
// We can access arch information parsed by the beautiful arch parser
const { fieldFromTheArch } = archInfo;
this.fieldFromTheArch = fieldFromTheArch;
this.fields = fields;
this.domain = domain;
this.keepLast = new KeepLast();
}
async load() {
// The keeplast protect against concurrency call
const { length, records } = await this.keepLast.add(
this.orm.webSearchRead(this.resModel, this.domain, [this.fieldsFromTheArch], {})
);
this.records = records;
this.recordsLength = length;
}
}
.. note::
For advanced cases, instead of creating a model from scratch, it is also possible to use
`RelationalModel`, which is used by other views.
#. Create the arch parser.
The role of the arch parser is to parse the arch view so the view has access to the information.
.. code-block:: js
:caption: :file:`beautiful_arch_parser.js`
/** @odoo-module */
import { XMLParser } from "@web/core/utils/xml";
export class BeautifulArchParser extends XMLParser {
parse(arch) {
const xmlDoc = this.parseXML(arch);
const fieldFromTheArch = xmlDoc.getAttribute("fieldFromTheArch");
return {
fieldFromTheArch,
};
}
}
#. Create the view and combine all the pieces together, then register the view in the views
registry.
.. code-block:: js
:caption: :file:`beautiful_view.js`
/** @odoo-module */
import { registry } from "@web/core/registry";
import { BeautifulController } from "./beautiful_controller";
import { BeautifulArchParser } from "./beautiful_arch_parser";
import { BeautifylModel } from "./beautiful_model";
import { BeautifulRenderer } from "./beautiful_renderer";
export const beautifulView = {
type: "beautiful",
display_name: "Beautiful",
icon: "fa fa-picture-o", // the icon that will be displayed in the Layout panel
multiRecord: true,
Controller: BeautifulController,
ArchParser: BeautifulArchParser,
Model: BeautifulModel,
Renderer: BeautifulRenderer,
props(genericProps, view) {
const { ArchParser } = view;
const { arch } = genericProps;
const archInfo = new ArchParser().parse(arch);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer,
archInfo,
};
},
};
registry.category("views").add("beautifulView", beautifulView);
#. Use the view in an arch.
.. code-block:: xml
...
<beautiful fieldFromTheArch="res.partner"/>
...

View File

@@ -353,87 +353,6 @@ While formatting the template differently would prevent such vulnerabilities.
font-weight: bold;
}
Creating safe content using :class:`~markupsafe.Markup`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See the `official documentation <https://markupsafe.palletsprojects.com/>`_ for
explanations, but the big advantage of
:class:`~markupsafe.Markup` is that it's a very rich type overrinding
:class:`str` operations to *automatically escape parameters*.
This means that it's easy to create *safe* html snippets by using
:class:`~markupsafe.Markup` on a string literal and "formatting in"
user-provided (and thus potentially unsafe) content:
.. code-block:: pycon
>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
>>> Markup('<em>Hello</em> %s') % '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
though it is a very good thing, note that the effects can be odd at times:
.. code-block:: pycon
>>> Markup('<a>').replace('>', 'x')
Markup('<a>')
>>> Markup('<a>').replace(Markup('>'), 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', '&')
Markup('<a&amp;')
.. tip:: Most of the content-safe APIs actually return a
:class:`~markupsafe.Markup` with all that implies.
The :class:`~markupsafe.escape` method (and its
alias :class:`~odoo.tools.misc.html_escape`) turns a `str` into
a :class:`~markupsafe.Markup` and escapes its content. It will not escape the
content of a :class:`~markupsafe.Markup` object.
.. code-block:: python
def get_name(self, to_html=False):
if to_html:
return Markup("<strong>%s</strong>") % self.name # escape the name
else:
return self.name
>>> record.name = "<R&D>"
>>> escape(record.get_name())
Markup("&lt;R&amp;D&gt;")
>>> escape(record.get_name(True))
Markup("<strong>&lt;R&amp;D&gt;</strong>") # HTML is kept
When generating HTML code, it is important to separate the structure (tags) from
the content (text).
.. code-block:: pycon
>>> Markup("<p>") + "Hello <R&D>" + Markup("</p>")
Markup('<p>Hello &lt;R&amp;D&gt;</p>')
>>> Markup("%s <br/> %s") % ("<R&D>", Markup("<p>Hello</p>"))
Markup('&lt;R&amp;D&gt; <br/> <p>Hello</p>')
>>> escape("<R&D>")
Markup('&lt;R&amp;D&gt;')
>>> escape(_("List of Tasks on project %s: %s")) % (
... project.name,
... Markup("<ul>%s</ul>") % Markup().join([Markup("<li>%s</li>") % t.name for t in project.task_ids])
... )
Markup('Liste de tâches pour le projet &lt;R&amp;D&gt;: <ul><li>First &lt;R&amp;D&gt; task</li></ul>')
>>> Markup("<p>Foo %</p>" % bar) # bad, bar is not escaped
>>> Markup("<p>Foo %</p>") % bar # good, bar is escaped if text and kept if markup
>>> link = Markup("<a>%s</a>") % self.name
>>> message = "Click %s" % link # bad, message is text and Markup did nothing
>>> message = escape("Click %s") % link # good, format two markup objects together
>>> Markup(f"<p>Foo {self.bar}</p>") # bad, bar is inserted before escaping
>>> Markup("<p>Foo {bar}</p>").format(bar=self.bar) # good, sorry no fstring
Escaping vs Sanitizing
----------------------
@@ -461,10 +380,10 @@ variable contains *TEXT* and which contains *CODE*.
# Escaping turns it into CODE, good!
>>> code = html_escape(data)
>>> code
Markup('&lt;R&amp;D&gt;')
'&lt;R&amp;D&gt;'
# Now you can mix it with other code...
>>> self.website_description = Markup("<strong>%s</strong>") % code
>>> self.message_post(body="<strong>%s</strong>" % code)
**Sanitizing** converts *CODE* to *SAFER CODE* (but not necessary *safe* code).
It does not work on *TEXT*. Sanitizing is only necessary when *CODE* is
@@ -479,11 +398,11 @@ expected.
# Sanitizing without escaping is BROKEN: data is corrupted!
>>> html_sanitize(data)
Markup('')
''
# Sanitizing *after* escaping is OK!
>>> html_sanitize(code)
Markup('<p>&lt;R&amp;D&gt;</p>')
'<p>&lt;R&amp;D&gt;</p>'
Sanitizing can break features, depending on whether the *CODE* is expected to
contain patterns that are not safe. That's why `fields.Html` and
@@ -495,11 +414,11 @@ likely it is to break things.
.. code-block:: python
>>> code = "<p class='text-warning'>Important Information</p>"
>>code = "<p class='text-warning'>Important Information</p>"
# this will remove the style, which may break features
# but is necessary if the source is untrusted
>>> html_sanitize(code, strip_classes=True)
Markup('<p>Important Information</p>')
>> html_sanitize(code, strip_classes=True)
'<p>Important Information</p>'
Evaluating content
------------------

View File

@@ -47,16 +47,16 @@ The test runner will simply run any test case, as described in the official
`unittest documentation`_, but Odoo provides a number of utilities and helpers
related to testing Odoo content (modules, mainly):
.. autoclass:: odoo.tests.TransactionCase
.. autoclass:: odoo.tests.common.TransactionCase
:members: browse_ref, ref
.. autoclass:: odoo.tests.SingleTransactionCase
.. autoclass:: odoo.tests.common.SingleTransactionCase
:members: browse_ref, ref
.. autoclass:: odoo.tests.HttpCase
.. autoclass:: odoo.tests.common.HttpCase
:members: browse_ref, ref, url_open, browser_js
.. autofunction:: odoo.tests.tagged
.. autofunction:: odoo.tests.common.tagged
By default, tests are run once right after the corresponding module has been
installed. Test cases can also be configured to run after all modules have
@@ -72,10 +72,10 @@ been installed, and not run right after the module installation::
Page = self.env['website.page']
The most common situation is to use
:class:`~odoo.tests.TransactionCase` and test a property of a model
:class:`~odoo.tests.common.TransactionCase` and test a property of a model
in each method::
class TestModelA(TransactionCase):
class TestModelA(common.TransactionCase):
def test_some_action(self):
record = self.env['model.a'].create({'field': 'value'})
record.some_action()
@@ -89,13 +89,13 @@ in each method::
Test methods must start with ``test_``
.. autoclass:: odoo.tests.Form
.. autoclass:: odoo.tests.common.Form
:members:
.. autoclass:: odoo.tests.M2MProxy
.. autoclass:: odoo.tests.common.M2MProxy
:members: add, remove, clear
.. autoclass:: odoo.tests.O2MProxy
.. autoclass:: odoo.tests.common.O2MProxy
:members: new, edit, remove
Running tests
@@ -115,9 +115,9 @@ Test selection
In Odoo, Python tests can be tagged to facilitate the test selection when
running tests.
Subclasses of :class:`odoo.tests.BaseCase` (usually through
:class:`~odoo.tests.TransactionCase` or
:class:`~odoo.tests.HttpCase`) are automatically tagged with
Subclasses of :class:`odoo.tests.common.BaseCase` (usually through
:class:`~odoo.tests.common.TransactionCase` or
:class:`~odoo.tests.common.HttpCase`) are automatically tagged with
``standard`` and ``at_install`` by default.
Invocation
@@ -132,12 +132,12 @@ This option defaults to ``+standard`` meaning tests tagged ``standard``
(explicitly or implicitly) will be run by default when starting Odoo
with :option:`--test-enable <odoo-bin --test-enable>`.
When writing tests, the :func:`~odoo.tests.tagged` decorator can be
When writing tests, the :func:`~odoo.tests.common.tagged` decorator can be
used on **test classes** to add or remove tags.
The decorator's arguments are tag names, as strings.
.. danger:: :func:`~odoo.tests.tagged` is a class decorator, it has no
.. danger:: :func:`~odoo.tests.common.tagged` is a class decorator, it has no
effect on functions or methods
Tags can be prefixed with the minus (``-``) sign, to *remove* them instead of
@@ -180,7 +180,7 @@ ones:
$ odoo-bin --test-tags 'standard,-slow'
When you write a test that does not inherit from the
:class:`~odoo.tests.BaseCase`, this test will not have the default tags,
:class:`~odoo.tests.common.BaseCase`, this test will not have the default tags,
you have to add them explicitly to have the test included in the default test
suite. This is a common issue when using a simple ``unittest.TestCase`` as
they're not going to get run:
@@ -230,7 +230,7 @@ Special tags
~~~~~~~~~~~~
- ``standard``: All Odoo tests that inherit from
:class:`~odoo.tests.BaseCase` are implicitly tagged standard.
:class:`~odoo.tests.common.BaseCase` are implicitly tagged standard.
:option:`--test-tags <odoo-bin --test-tags>` also defaults to ``standard``.
That means untagged test will be executed by default when tests are enabled.
@@ -715,7 +715,7 @@ Python
~~~~~~
To start a tour from a python test, make the class inherit from
:class:`~odoo.tests.HTTPCase`, and call `start_tour`:
:class:`~odoo.tests.common.HTTPCase`, and call `start_tour`:
.. code-block:: python
@@ -858,7 +858,7 @@ Query counts
One of the ways to test performance is to measure database queries. Manually, this can be tested with the
`--log-sql` CLI parameter. If you want to establish the maximum number of queries for an operation,
you can use the :meth:`~odoo.tests.BaseCase.assertQueryCount` method, integrated in Odoo test classes.
you can use the :meth:`~odoo.tests.common.BaseCase.assertQueryCount` method, integrated in Odoo test classes.
.. code-block:: python

View File

@@ -1728,6 +1728,12 @@ Possible children of the view element are:
``name`` (required)
the name of the field to fetch
``allow_group_range_value`` (optional)
whether a ``date`` or ``datetime`` field allows a value computed from a
group range (which consists of the first and last dates of the group).
Enables the 'quick create' and 'drag and drop' features when the kanban
view is grouped by that field. Default: false.
.. include:: views/header_buttons.rst
.. note::

View File

@@ -15,7 +15,6 @@ JavaScript framework
frontend/services
frontend/hooks
frontend/patching_code
frontend/javascript_cheatsheet
frontend/javascript_reference
frontend/mobile
frontend/qweb

View File

@@ -1,546 +0,0 @@
.. _reference/jscs:
=====================
Javascript Cheatsheet
=====================
There are many ways to solve a problem in JavaScript, and in Odoo. However, the
Odoo framework was designed to be extensible (this is a pretty big constraint),
and some common problems have a nice standard solution. The standard solution
has probably the advantage of being easy to understand for an odoo developer,
and will probably keep working when Odoo is modified.
This document tries to explain the way one could solve some of these issues.
Note that this is not a reference. This is just a random collection of recipes,
or explanations on how to proceed in some cases.
First of all, remember that the first rule of customizing odoo with JS is:
*try to do it in python*. This may seem strange, but the python framework is
quite extensible, and many behaviours can be done simply with a touch of xml or
python. This has usually a lower cost of maintenance than working with JS:
- the JS framework tends to change more, so JS code needs to be more frequently
updated
- it is often more difficult to implement a customized behaviour if it needs to
communicate with the server and properly integrate with the javascript framework.
There are many small details taken care by the framework that customized code
needs to replicate. For example, responsiveness, or updating the url, or
displaying data without flickering.
.. note:: This document does not really explain any concepts. This is more a
cookbook. For more details, please consult the javascript reference
page (see :doc:`javascript_reference`)
Creating a new field widget
===========================
This is probably a really common usecase: we want to display some information in
a form view in a really specific (maybe business dependent) way. For example,
assume that we want to change the text color depending on some business condition.
This can be done in three steps: creating a new widget, registering it in the
field registry, then adding the widget to the field in the form view
- creating a new widget:
This can be done by extending a widget:
.. code-block:: javascript
var FieldChar = require('web.basic_fields').FieldChar;
var CustomFieldChar = FieldChar.extend({
_renderReadonly: function () {
// implement some custom logic here
},
});
- registering it in the field registry:
The web client needs to know the mapping between a widget name and its
actual class. This is done by a registry:
.. code-block:: javascript
var fieldRegistry = require('web.field_registry');
fieldRegistry.add('my-custom-field', CustomFieldChar);
- adding the widget in the form view
.. code-block:: xml
<field name="somefield" widget="my-custom-field"/>
Note that only the form, list and kanban views use this field widgets registry.
These views are tightly integrated, because the list and kanban views can
appear inside a form view).
Modifying an existing field widget
==================================
Another use case is that we want to modify an existing field widget. For
example, the voip addon in odoo need to modify the FieldPhone widget to add the
possibility to easily call the given number on voip. This is done by *including*
the FieldPhone widget, so there is no need to change any existing form view.
Field Widgets (instances of (subclass of) AbstractField) are like every other
widgets, so they can be monkey patched. This looks like this:
.. code-block:: javascript
var basic_fields = require('web.basic_fields');
var Phone = basic_fields.FieldPhone;
Phone.include({
events: _.extend({}, Phone.prototype.events, {
'click': '_onClick',
}),
_onClick: function (e) {
if (this.mode === 'readonly') {
e.preventDefault();
var phoneNumber = this.value;
// call the number on voip...
}
},
});
Note that there is no need to add the widget to the registry, since it is already
registered.
Modifying a main widget from the interface
==========================================
Another common usecase is the need to customize some elements from the user
interface. For example, adding a message in the home menu. The usual process
in this case is again to *include* the widget. This is the only way to do it,
since there are no registries for those widgets.
This is usually done with code looking like this:
.. code-block:: javascript
var HomeMenu = require('web_enterprise.HomeMenu');
HomeMenu.include({
render: function () {
this._super();
// do something else here...
},
});
Creating a new view (from scratch)
==================================
Creating a new view is a more advanced topic. This cheatsheet will only
highlight the steps that will probably need to be done (in no particular order):
- adding a new view type to the field ``type`` of ``ir.ui.view``::
class View(models.Model):
_inherit = 'ir.ui.view'
type = fields.Selection(selection_add=[('map', "Map")])
- adding the new view type to the field ``view_mode`` of ``ir.actions.act_window.view``::
class ActWindowView(models.Model):
_inherit = 'ir.actions.act_window.view'
view_mode = fields.Selection(selection_add=[('map', "Map")])
- creating the four main pieces which makes a view (in JavaScript):
we need a view (a subclass of ``AbstractView``, this is the factory), a
renderer (from ``AbstractRenderer``), a controller (from ``AbstractController``)
and a model (from ``AbstractModel``). I suggest starting by simply
extending the superclasses:
.. code-block:: javascript
var AbstractController = require('web.AbstractController');
var AbstractModel = require('web.AbstractModel');
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractView = require('web.AbstractView');
var MapController = AbstractController.extend({});
var MapRenderer = AbstractRenderer.extend({});
var MapModel = AbstractModel.extend({});
var MapView = AbstractView.extend({
config: {
Model: MapModel,
Controller: MapController,
Renderer: MapRenderer,
},
});
- adding the view to the registry:
As usual, the mapping between a view type and the actual class needs to be
updated:
.. code-block:: javascript
var viewRegistry = require('web.view_registry');
viewRegistry.add('map', MapView);
- implementing the four main classes:
The ``View`` class needs to parse the ``arch`` field and setup the other
three classes. The ``Renderer`` is in charge of representing the data in
the user interface, the ``Model`` is supposed to talk to the server, to
load data and process it. And the ``Controller`` is there to coordinate,
to talk to the web client, ...
- creating some views in the database:
.. code-block:: xml
<record id="customer_map_view" model="ir.ui.view">
<field name="name">customer.map.view</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<map latitude="partner_latitude" longitude="partner_longitude">
<field name="name"/>
</map>
</field>
</record>
Customizing an existing view
============================
Assume we need to create a custom version of a generic view. For example, a
kanban view with some extra *ribbon-like* widget on top (to display some
specific custom information). In that case, this can be done with 3 steps:
extend the kanban view (which also probably mean extending controllers/renderers
and/or models), then registering the view in the view registry, and finally,
using the view in the kanban arch (a specific example is the helpdesk dashboard).
- extending a view:
Here is what it could look like:
.. code-block:: javascript
var HelpdeskDashboardRenderer = KanbanRenderer.extend({
...
});
var HelpdeskDashboardModel = KanbanModel.extend({
...
});
var HelpdeskDashboardController = KanbanController.extend({
...
});
var HelpdeskDashboardView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Model: HelpdeskDashboardModel,
Renderer: HelpdeskDashboardRenderer,
Controller: HelpdeskDashboardController,
}),
});
- adding it to the view registry:
as usual, we need to inform the web client of the mapping between the name
of the views and the actual class.
.. code-block:: javascript
var viewRegistry = require('web.view_registry');
viewRegistry.add('helpdesk_dashboard', HelpdeskDashboardView);
- using it in an actual view:
we now need to inform the web client that a specific ``ir.ui.view`` needs to
use our new class. Note that this is a web client specific concern. From
the point of view of the server, we still have a kanban view. The proper
way to do this is by using a special attribute ``js_class`` (which will be
renamed someday into ``widget``, because this is really not a good name) on
the root node of the arch:
.. code-block:: xml
<record id="helpdesk_team_view_kanban" model="ir.ui.view" >
...
<field name="arch" type="xml">
<kanban js_class="helpdesk_dashboard">
...
</kanban>
</field>
</record>
.. note::
Note: you can change the way the view interprets the arch structure. However,
from the server point of view, this is still a view of the same base type,
subjected to the same rules (rng validation, for example). So, your views still
need to have a valid arch field.
Promises and asynchronous code
==============================
For a very good and complete introduction to promises, please read this excellent article https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch3.md
Creating new Promises
---------------------
- turn a constant into a promise
There are 2 static functions on Promise that create a resolved or rejected promise based on a constant:
.. code-block:: javascript
var p = Promise.resolve({blabla: '1'}); // creates a resolved promise
p.then(function (result) {
console.log(result); // --> {blabla: '1'};
});
var p2 = Promise.reject({error: 'error message'}); // creates a rejected promise
p2.catch(function (reason) {
console.log(reason); // --> {error: 'error message');
});
.. note:: Note that even if the promises are created already resolved or rejected, the `then` or `catch` handlers will still be called asynchronously.
- based on an already asynchronous code
Suppose that in a function you must do a rpc, and when it is completed set the result on this.
The `this._rpc` is a function that returns a `Promise`.
.. code-block:: javascript
function callRpc() {
var self = this;
return this._rpc(...).then(function (result) {
self.myValueFromRpc = result;
});
}
- for callback based function
Suppose that you were using a function `this.close` that takes as parameter a callback that is called when the closing is finished.
Now suppose that you are doing that in a method that must send a promise that is resolved when the closing is finished.
.. code-block:: javascript
:linenos:
function waitForClose() {
var self = this;
return new Promise (function(resolve, reject) {
self.close(resolve);
});
}
* line 2: we save the `this` into a variable so that in an inner function, we can access the scope of our component
* line 3: we create and return a new promise. The constructor of a promise takes a function as parameter. This function itself has 2 parameters that we called here `resolve` and `reject`
- `resolve` is a function that, when called, puts the promise in the resolved state.
- `reject` is a function that, when called, puts the promise in the rejected state. We do not use reject here and it can be omitted.
* line 4: we are calling the function close on our object. It takes a function as parameter (the callback) and it happens that resolve is already a function, so we can pass it directly. To be clearer, we could have written:
.. code-block:: javascript
return new Promise (function (resolve) {
self.close(function () {
resolve();
});
});
- creating a promise generator (calling one promise after the other *in sequence* and waiting for the last one)
Suppose that you need to loop over an array, do an operation *in sequence* and resolve a promise when the last operation is done.
.. code-block:: javascript
function doStuffOnArray(arr) {
var done = Promise.resolve();
arr.forEach(function (item) {
done = done.then(function () {
return item.doSomethingAsynchronous();
});
});
return done;
}
This way, the promise you return is effectively the last promise.
- creating a promise, then resolving it outside the scope of its definition (anti-pattern)
.. note:: we do not recommend using this, but sometimes it is useful. Think carefully for alternatives first...
.. code-block:: javascript
...
var resolver, rejecter;
var prom = new Promise(function (resolve, reject){
resolver = resolve;
rejecter = reject;
});
...
resolver("done"); // will resolve the promise prom with the result "done"
rejecter("error"); // will reject the promise prom with the reason "error"
Waiting for Promises
--------------------
- waiting for a number of Promises
if you have multiple promises that all need to be waited, you can convert them into a single promise that will be resolved when all the promises are resolved using Promise.all(arrayOfPromises).
.. code-block:: javascript
var prom1 = doSomethingThatReturnsAPromise();
var prom2 = Promise.resolve(true);
var constant = true;
var all = Promise.all([prom1, prom2, constant]); // all is a promise
// results is an array, the individual results correspond to the index of their
// promise as called in Promise.all()
all.then(function (results) {
var prom1Result = results[0];
var prom2Result = results[1];
var constantResult = results[2];
});
return all;
- waiting for a part of a promise chain, but not another part
If you have an asynchronous process that you want to wait to do something, but you also want to return to the caller before that something is done.
.. code-block:: javascript
function returnAsSoonAsAsyncProcessIsDone() {
var prom = AsyncProcess();
prom.then(function (resultOfAsyncProcess) {
return doSomething();
});
/* returns prom which will only wait for AsyncProcess(),
and when it will be resolved, the result will be the one of AsyncProcess */
return prom;
}
Error handling
--------------
- in general in promises
The general idea is that a promise should not be rejected for control flow, but should only be rejected for errors.
When that is the case, you would have multiple resolutions of your promise with, for instance status codes that you would have to check in the `then` handlers and a single `catch` handler at the end of the promise chain.
.. code-block:: javascript
function a() {
x.y(); // <-- this is an error: x is undefined
return Promise.resolve(1);
}
function b() {
return Promise.reject(2);
}
a().catch(console.log); // will log the error in a
a().then(b).catch(console.log); // will log the error in a, the then is not executed
b().catch(console.log); // will log the rejected reason of b (2)
Promise.resolve(1)
.then(b) // the then is executed, it executes b
.then(...) // this then is not executed
.catch(console.log); // will log the rejected reason of b (2)
- in Odoo specifically
In Odoo, it happens that we use promise rejection for control flow, like in mutexes and other concurrency primitives defined in module `web.concurrency`
We also want to execute the catch for *business* reasons, but not when there is a coding error in the definition of the promise or of the handlers.
For this, we have introduced the concept of `guardedCatch`. It is called like `catch` but not when the rejected reason is an error
.. code-block:: javascript
function blabla() {
if (someCondition) {
return Promise.reject("someCondition is truthy");
}
return Promise.resolve();
}
// ...
var promise = blabla();
promise.then(function (result) { console.log("everything went fine"); })
// this will be called if blabla returns a rejected promise, but not if it has an error
promise.guardedCatch(function (reason) { console.log(reason); });
// ...
var anotherPromise =
blabla().then(function () { console.log("everything went fine"); })
// this will be called if blabla returns a rejected promise,
// but not if it has an error
.guardedCatch(console.log);
.. code-block:: javascript
var promiseWithError = Promise.resolve().then(function () {
x.y(); // <-- this is an error: x is undefined
});
promiseWithError.guardedCatch(function (reason) {console.log(reason);}); // will not be called
promiseWithError.catch(function (reason) {console.log(reason);}); // will be called
Testing asynchronous code
-------------------------
- using promises in tests
In the tests code, we support the latest version of Javascript, including primitives like `async` and `await`. This makes using and waiting for promises very easy.
Most helper methods also return a promise (either by being marked `async` or by returning a promise directly.
.. code-block:: javascript
var testUtils = require('web.test_utils');
QUnit.test("My test", async function (assert) {
// making the function async has 2 advantages:
// 1) it always returns a promise so you don't need to define `var done = assert.async()`
// 2) it allows you to use the `await`
assert.expect(1);
var form = await testUtils.createView({ ... });
await testUtils.form.clickEdit(form);
await testUtils.form.click('jquery selector');
assert.containsOnce('jquery selector');
form.destroy();
});
QUnit.test("My test - no async - no done", function (assert) {
// this function is not async, but it returns a promise.
// QUnit will wait for for this promise to be resolved.
assert.expect(1);
return testUtils.createView({ ... }).then(function (form) {
return testUtils.form.clickEdit(form).then(function () {
return testUtils.form.click('jquery selector').then(function () {
assert.containsOnce('jquery selector');
form.destroy();
});
});
});
});
QUnit.test("My test - no async", function (assert) {
// this function is not async and does not return a promise.
// we have to use the done function to signal QUnit that the test is async and will be finished inside an async callback
assert.expect(1);
var done = assert.async();
testUtils.createView({ ... }).then(function (form) {
testUtils.form.clickEdit(form).then(function () {
testUtils.form.click('jquery selector').then(function () {
assert.containsOnce('jquery selector');
form.destroy();
done();
});
});
});
});
as you can see, the nicer form is to use `async/await` as it is clearer and shorter to write.

View File

@@ -372,6 +372,58 @@ templates:
* :func:`~odoo.tools.pycompat.to_text` does not mark the content as safe, but
will not strip that information from safe content.
Creating safe content using :class:`~markupsafe.Markup`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See the official documentation for explanations, but the big advantage of
:class:`~markupsafe.Markup` is that it's a very rich type overrinding
:class:`str` operations to *automatically escape parameters*.
This means that it's easy to create *safe* html snippets by using
:class:`~markupsafe.Markup` on a string literal and "formatting in"
user-provided (and thus potentially unsafe) content:
.. code-block:: pycon
>>> Markup('<em>Hello</em> ') + '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
>>> Markup('<em>Hello</em> %s') % '<foo>'
Markup('<em>Hello</em> &lt;foo&gt;')
though it is a very good thing, note that the effects can be odd at times:
.. code-block:: pycon
>>> Markup('<a>').replace('>', 'x')
Markup('<a>')
>>> Markup('<a>').replace(Markup('>'), 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', 'x')
Markup('<ax')
>>> Markup('<a&gt;').replace('>', '&')
Markup('<a&amp;')
.. tip:: Most of the content-safe APIs actually return a
:class:`~markupsafe.Markup` with all that implies.
Javascript
----------
.. todo:: what APIs do we end up considering OK there?
.. todo:: talk about vdom thingies?
.. warning::
Due to the lack of operator overriding, :js:class:`Markup` is a much more
limited type than :class:`~markupsafe.Markup`.
Therefore it doesn't override methods either, and any operation involving
:js:class:`Markup` will return a normal :js:class:`String` (and in reality
not even that, but a "primitive string").
This means the fallback is safe, but it is easy to trigger double-escaping
when working with :js:class:`Markup` objects.
forcing double-escaping
-----------------------

View File

@@ -46,4 +46,4 @@ developer/howtos/discover_js_framework/07_testing.rst developer/tutorials/discov
# developer/reference/frontend
developer/reference/frontend/icons_library.rst contributing/development/ui/icons.rst # Odoo UI icons -> UI Icons
developer/reference/frontend/javascript_cheatsheet.rst developer/howtos/javascript_create_field.rst # refactor JavaScript cheatsheet into howtos