[FIX] *: views: add component instance to rendering context
Owl last update fixes the leaking `this` in rendering context of templates called with `t-call-context` [1]. Since [2], we use the `t-call-context` directive to control the rendering context of templates compiled from an arch (e.g. in form and kanban views). We can now better control what people use in archs, where we don't want them to access js implementation details, as `this` is no longer available. However, the component instance still needs to be accessible in the compiled template. We thus add the `__comp__` key in the rendering context. Since we do not want people to access it in archs, we add a check in the view validation that this string isn't used in dynamic attributes. [1] https://github.com/odoo/owl/commit/df59ec49aefce2e0913fdc1792d42b9680fb28b6 [2] https://github.com/odoo/odoo/commit/4c5b867ff6b0b674cb83d1a1262ae354ebaa6d57 X-original-commit: 264f313012aa99449d1576ae1110252c13755142 Part-of: odoo/odoo#110196
This commit is contained in:
@@ -45,6 +45,7 @@ export class ActivityRecord extends Component {
|
||||
user_context: this.user.context,
|
||||
widget: this.widget,
|
||||
luxon,
|
||||
__comp__: Object.assign(Object.create(this), { this: this }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<t t-name="mail.ActivityRecord" owl="1">
|
||||
<td class="o_activity_record p-3 cursor-pointer" t-on-click="() => this.props.openRecord(this.props.record)">
|
||||
<t t-call="{{ recordTemplate }}" t-call-context="getRenderingContext()"/>
|
||||
<t t-call="{{ recordTemplate }}" t-call-context="this.getRenderingContext()"/>
|
||||
</td>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export class MailFormCompiler extends ViewCompiler {
|
||||
const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer");
|
||||
if (chatterContainerHookXml) {
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
"t-if": `!this.hasAttachmentViewer() and this.uiService.size >= ${SIZES.XXL}`,
|
||||
"t-if": `!__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
|
||||
"t-attf-class": "o-aside",
|
||||
});
|
||||
const chatterContainerXml = chatterContainerHookXml.querySelector("ChatterContainer");
|
||||
@@ -99,7 +99,7 @@ export class MailFormCompiler extends ViewCompiler {
|
||||
const attachmentViewHookXml = res.querySelector(".o_attachment_preview");
|
||||
if (attachmentViewHookXml) {
|
||||
setAttributes(attachmentViewHookXml, {
|
||||
"t-if": `this.hasAttachmentViewer()`,
|
||||
"t-if": `__comp__.hasAttachmentViewer()`,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
@@ -112,7 +112,7 @@ export class MailFormCompiler extends ViewCompiler {
|
||||
let compiledChild = this.compileNode(child, params, false);
|
||||
compiledChild = this.applyInvisible(invisible, compiledChild, {
|
||||
...params,
|
||||
recordExpr: "this.model.root",
|
||||
recordExpr: "__comp__.model.root",
|
||||
});
|
||||
append(compiledRoot, compiledChild);
|
||||
}
|
||||
@@ -122,16 +122,16 @@ export class MailFormCompiler extends ViewCompiler {
|
||||
compileChatter(node) {
|
||||
return compileChatter(node, {
|
||||
chatter: "chatter",
|
||||
threadId: "this.model.root.resId or undefined",
|
||||
threadModel: "this.model.root.resModel",
|
||||
webRecord: "this.model.root",
|
||||
threadId: "__comp__.model.root.resId or undefined",
|
||||
threadModel: "__comp__.model.root.resModel",
|
||||
webRecord: "__comp__.model.root",
|
||||
});
|
||||
}
|
||||
|
||||
compileAttachmentPreview(node) {
|
||||
return compileAttachmentPreview(node, {
|
||||
threadId: "this.model.root.resId or undefined",
|
||||
threadModel: "this.model.root.resModel",
|
||||
threadId: "__comp__.model.root.resId or undefined",
|
||||
threadModel: "__comp__.model.root.resModel",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -140,10 +140,10 @@ registry.category("form_compilers").add("chatter_compiler", {
|
||||
selector: "div.oe_chatter",
|
||||
fn: (node) =>
|
||||
compileChatter(node, {
|
||||
chatter: "this.props.chatter",
|
||||
threadId: "this.props.record.resId or undefined",
|
||||
threadModel: "this.props.record.resModel",
|
||||
webRecord: "this.props.record",
|
||||
chatter: "__comp__.props.chatter",
|
||||
threadId: "__comp__.props.record.resId or undefined",
|
||||
threadModel: "__comp__.props.record.resModel",
|
||||
webRecord: "__comp__.props.record",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -151,8 +151,8 @@ registry.category("form_compilers").add("attachment_preview_compiler", {
|
||||
selector: "div.o_attachment_preview",
|
||||
fn: (node) =>
|
||||
compileAttachmentPreview(node, {
|
||||
threadId: "this.props.record.resId or undefined",
|
||||
threadModel: "this.props.record.resModel",
|
||||
threadId: "__comp__.props.record.resId or undefined",
|
||||
threadModel: "__comp__.props.record.resModel",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ patch(FormCompiler.prototype, "mail", {
|
||||
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
|
||||
sheetBgChatterContainerHookXml.classList.add("o-isInFormSheetBg");
|
||||
setAttributes(sheetBgChatterContainerHookXml, {
|
||||
"t-if": `this.props.hasAttachmentViewer`,
|
||||
"t-if": `__comp__.props.hasAttachmentViewer`,
|
||||
});
|
||||
append(formSheetBgXml, sheetBgChatterContainerHookXml);
|
||||
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(
|
||||
@@ -195,7 +195,7 @@ patch(FormCompiler.prototype, "mail", {
|
||||
}
|
||||
// after sheet bg (standard position, below form)
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
"t-if": `!this.props.hasAttachmentViewer and this.uiService.size < ${SIZES.XXL}`,
|
||||
"t-if": `!__comp__.props.hasAttachmentViewer and __comp__.uiService.size < ${SIZES.XXL}`,
|
||||
});
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
return res;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<t t-inherit="web.FormView" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_form_view_container')]" position="after">
|
||||
<t t-if="mailTemplate">
|
||||
<t t-call="{{ mailTemplate }}" />
|
||||
<t t-call="{{ mailTemplate }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
|
||||
|
||||
+9
-9
@@ -40,7 +40,7 @@ export class ProjectSharingChatterCompiler extends ViewCompiler {
|
||||
const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer");
|
||||
if (chatterContainerHookXml) {
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
"t-if": `this.uiService.size >= ${SIZES.XXL}`,
|
||||
"t-if": `__comp__.uiService.size >= ${SIZES.XXL}`,
|
||||
});
|
||||
chatterContainerHookXml.classList.add('overflow-x-hidden', 'overflow-y-auto', 'o-aside', 'h-100');
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export class ProjectSharingChatterCompiler extends ViewCompiler {
|
||||
let compiledChild = this.compileNode(child, params, false);
|
||||
compiledChild = this.applyInvisible(invisible, compiledChild, {
|
||||
...params,
|
||||
recordExpr: "this.model.root",
|
||||
recordExpr: "__comp__.model.root",
|
||||
});
|
||||
append(compiledRoot, compiledChild);
|
||||
}
|
||||
@@ -63,9 +63,9 @@ export class ProjectSharingChatterCompiler extends ViewCompiler {
|
||||
|
||||
compileChatter(node) {
|
||||
return compileChatter(node, {
|
||||
resId: 'this.model.root.resId or undefined',
|
||||
resModel: 'this.model.root.resModel',
|
||||
projectSharingId: 'this.model.root.context.active_id',
|
||||
resId: '__comp__.model.root.resId or undefined',
|
||||
resModel: '__comp__.model.root.resModel',
|
||||
projectSharingId: '__comp__.model.root.context.active_id',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,9 @@ registry.category("form_compilers").add("portal_chatter_compiler", {
|
||||
selector: "div.oe_chatter",
|
||||
fn: (node) =>
|
||||
compileChatter(node, {
|
||||
resId: "this.props.record.resId or undefined",
|
||||
resModel: "this.props.record.resModel",
|
||||
projectSharingId: "this.props.record.context.active_id",
|
||||
resId: "__comp__.props.record.resId or undefined",
|
||||
resModel: "__comp__.props.record.resModel",
|
||||
projectSharingId: "__comp__.props.record.context.active_id",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -97,7 +97,7 @@ patch(FormCompiler.prototype, 'project_sharing_chatter', {
|
||||
}
|
||||
// after sheet bg (standard position, below form)
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-if': `this.uiService.size < ${SIZES.XXL}`,
|
||||
't-if': `__comp__.uiService.size < ${SIZES.XXL}`,
|
||||
});
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
return res;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<t t-inherit="web.FormView" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_form_view_container')]" position="after">
|
||||
<t t-if="mailTemplate">
|
||||
<t t-call="{{ mailTemplate }}" />
|
||||
<t t-call="{{ mailTemplate }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
@@ -76,13 +76,13 @@ export class FormCompiler extends ViewCompiler {
|
||||
} else {
|
||||
labelText = labelText
|
||||
? toStringExpression(labelText)
|
||||
: `this.props.record.fields['${fieldName}'].string`;
|
||||
: `__comp__.props.record.fields['${fieldName}'].string`;
|
||||
}
|
||||
const formLabel = createElement("FormLabel", {
|
||||
id: `'${fieldId}'`,
|
||||
fieldName: `'${fieldName}'`,
|
||||
record: `this.props.record`,
|
||||
fieldInfo: `this.props.archInfo.fieldNodes['${fieldId}']`,
|
||||
record: `__comp__.props.record`,
|
||||
fieldInfo: `__comp__.props.archInfo.fieldNodes['${fieldId}']`,
|
||||
className: `"${label.className}"`,
|
||||
string: labelText,
|
||||
});
|
||||
@@ -140,7 +140,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
if (typeof invisible === "boolean") {
|
||||
isVisibleExpr = `${invisible ? false : true}`;
|
||||
} else {
|
||||
isVisibleExpr = `!this.evalDomainFromRecord(this.props.record,${JSON.stringify(
|
||||
isVisibleExpr = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
|
||||
invisible
|
||||
)})`;
|
||||
}
|
||||
@@ -176,8 +176,8 @@ export class FormCompiler extends ViewCompiler {
|
||||
|
||||
compileButton(el, params) {
|
||||
const compiled = super.compileButton(el, params);
|
||||
compiled.setAttribute("disable", "this.props.disableViewButtons");
|
||||
compiled.setAttribute("enable", "this.props.enableViewButtons");
|
||||
compiled.setAttribute("disable", "__comp__.props.disableViewButtons");
|
||||
compiled.setAttribute("enable", "__comp__.props.enableViewButtons");
|
||||
return compiled;
|
||||
}
|
||||
|
||||
@@ -219,13 +219,13 @@ export class FormCompiler extends ViewCompiler {
|
||||
compileForm(el, params) {
|
||||
const sheetNode = el.querySelector("sheet");
|
||||
const displayClasses = sheetNode
|
||||
? `d-flex {{ this.uiService.size < ${SIZES.XXL} ? "flex-column" : "flex-nowrap h-100" }}`
|
||||
? `d-flex {{ __comp__.uiService.size < ${SIZES.XXL} ? "flex-column" : "flex-nowrap h-100" }}`
|
||||
: "d-block";
|
||||
const stateClasses =
|
||||
"{{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}";
|
||||
"{{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}";
|
||||
const form = createElement("div", {
|
||||
"t-att-class": "this.props.class",
|
||||
"t-attf-class": `{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} ${displayClasses} ${stateClasses}`,
|
||||
"t-att-class": "__comp__.props.class",
|
||||
"t-attf-class": `{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} ${displayClasses} ${stateClasses}`,
|
||||
});
|
||||
if (!sheetNode) {
|
||||
for (const child of el.childNodes) {
|
||||
@@ -249,8 +249,9 @@ export class FormCompiler extends ViewCompiler {
|
||||
if (localization.multiLang) {
|
||||
const statusBar = form.querySelector(".o_form_statusbar");
|
||||
const translateAlert = createElement("t", {
|
||||
"t-if": "this.props.translateAlert",
|
||||
"t-if": "__comp__.props.translateAlert",
|
||||
"t-call": "web.TranslateAlert",
|
||||
"t-call-context": "__comp__",
|
||||
});
|
||||
if (statusBar) {
|
||||
statusBar.parentElement.insertBefore(translateAlert, statusBar.nextSibling);
|
||||
@@ -332,14 +333,14 @@ export class FormCompiler extends ViewCompiler {
|
||||
const props = {
|
||||
id: `${fieldId}`,
|
||||
fieldName: `'${fieldName}'`,
|
||||
record: `this.props.record`,
|
||||
record: `__comp__.props.record`,
|
||||
string: child.hasAttribute("string")
|
||||
? toStringExpression(child.getAttribute("string"))
|
||||
: `this.props.record.fields.${fieldName}.string`,
|
||||
fieldInfo: `this.props.archInfo.fieldNodes[${fieldId}]`,
|
||||
: `__comp__.props.record.fields.${fieldName}.string`,
|
||||
fieldInfo: `__comp__.props.archInfo.fieldNodes[${fieldId}]`,
|
||||
};
|
||||
mainSlot.setAttribute("props", objectToString(props));
|
||||
mainSlot.setAttribute("Component", "this.constructor.components.FormLabel");
|
||||
mainSlot.setAttribute("Component", "__comp__.constructor.components.FormLabel");
|
||||
mainSlot.setAttribute("subType", "'item_component'");
|
||||
}
|
||||
} else {
|
||||
@@ -360,7 +361,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
if (typeof invisible === "boolean") {
|
||||
isVisibleExpr = `${invisible ? false : true}`;
|
||||
} else {
|
||||
isVisibleExpr = `!this.evalDomainFromRecord(this.props.record,${JSON.stringify(
|
||||
isVisibleExpr = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
|
||||
invisible
|
||||
)})`;
|
||||
}
|
||||
@@ -504,11 +505,11 @@ export class FormCompiler extends ViewCompiler {
|
||||
|
||||
noteBook.setAttribute(
|
||||
"defaultPage",
|
||||
`this.props.record.isNew ? undefined : this.props.activeNotebookPages[${noteBookId}]`
|
||||
`__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[${noteBookId}]`
|
||||
);
|
||||
noteBook.setAttribute(
|
||||
"onPageUpdate",
|
||||
`(page) => this.props.onNotebookPageChange(${noteBookId}, page)`
|
||||
`(page) => __comp__.props.onNotebookPageChange(${noteBookId}, page)`
|
||||
);
|
||||
|
||||
for (const child of el.children) {
|
||||
@@ -539,7 +540,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
if (child.getAttribute("autofocus") === "autofocus") {
|
||||
noteBook.setAttribute(
|
||||
"defaultPage",
|
||||
`this.props.record.isNew ? "${pageId}" : (this.props.activeNotebookPages[${noteBookId}] || "${pageId}")`
|
||||
`__comp__.props.record.isNew ? "${pageId}" : (__comp__.props.activeNotebookPages[${noteBookId}] || "${pageId}")`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,7 +559,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
if (typeof invisible === "boolean") {
|
||||
isVisible = `${!invisible}`;
|
||||
} else {
|
||||
isVisible = `!this.evalDomainFromRecord(this.props.record,${JSON.stringify(
|
||||
isVisible = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
|
||||
invisible
|
||||
)})`;
|
||||
}
|
||||
@@ -632,7 +633,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
*/
|
||||
compileWidget(el) {
|
||||
const widget = super.compileWidget(el);
|
||||
widget.setAttribute("readonly", `!this.props.record.isInEdition`);
|
||||
widget.setAttribute("readonly", `!__comp__.props.record.isInEdition`);
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,16 +66,15 @@
|
||||
|
||||
<!--
|
||||
Used in FormRenderer
|
||||
Note: `props` is only accessible through `this` as we call the compiled template with
|
||||
`t-call-context="{}"`
|
||||
Note: `props` is only accessible through `this` as we call the compiled template a restricted context (t-call-context)
|
||||
-->
|
||||
<t t-name="web.TranslateAlert" owl="1">
|
||||
<div class="alert alert-info alert-dismissible" role="alertdialog">
|
||||
Please update translations of :
|
||||
<t t-foreach="Array.from(this.props.translateAlert.fields)" t-as="field" t-key="field.name">
|
||||
<t t-foreach="Array.from(props.translateAlert.fields)" t-as="field" t-key="field.name">
|
||||
<strong><a class="o_field_translate" t-att-name="field.name" href="#"><t t-esc="field.string"/><t t-if="!field_last">, </t></a></strong>
|
||||
</t>
|
||||
<button t-on-click="this.props.translateAlert.close" type="button" class="btn-close" aria-label="Close"></button>
|
||||
<button t-on-click="props.translateAlert.close" type="button" class="btn-close" aria-label="Close"></button>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -93,7 +93,7 @@ export class FormRenderer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
FormRenderer.template = xml`<t t-call="{{ templates.FormRenderer }}" t-call-context="{}" />`;
|
||||
FormRenderer.template = xml`<t t-call="{{ templates.FormRenderer }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }" />`;
|
||||
FormRenderer.components = {
|
||||
Field,
|
||||
FormLabel,
|
||||
|
||||
@@ -80,7 +80,7 @@ export class KanbanCompiler extends ViewCompiler {
|
||||
const strParams = Object.entries(nodeParams)
|
||||
.map(([k, v]) => [k, toStringExpression(v)].join(":"))
|
||||
.join(",");
|
||||
el.setAttribute("t-on-click", `()=>this.triggerAction({${strParams}})`);
|
||||
el.setAttribute("t-on-click", `()=>__comp__.triggerAction({${strParams}})`);
|
||||
|
||||
const compiled = createElement(el.nodeName);
|
||||
for (const { name, value } of el.attributes) {
|
||||
@@ -100,7 +100,10 @@ export class KanbanCompiler extends ViewCompiler {
|
||||
* @returns {Element}
|
||||
*/
|
||||
compileColorPicker() {
|
||||
return createElement("t", { "t-call": "web.KanbanColorPicker" });
|
||||
return createElement("t", {
|
||||
"t-call": "web.KanbanColorPicker",
|
||||
"t-call-context": "__comp__",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +120,7 @@ export class KanbanCompiler extends ViewCompiler {
|
||||
} else {
|
||||
compiled = super.compileField(el, params);
|
||||
const fieldId = el.getAttribute("field_id") || el.getAttribute("name");
|
||||
compiled.setAttribute("id", `'${fieldId}_' + this.props.record.id`);
|
||||
compiled.setAttribute("id", `'${fieldId}_' + __comp__.props.record.id`);
|
||||
}
|
||||
|
||||
const { bold, display } = extractAttributes(el, ["bold", "display"]);
|
||||
@@ -177,7 +180,7 @@ export class KanbanCompiler extends ViewCompiler {
|
||||
const compiled = this.compileGenericNode(el, params);
|
||||
const tname = el.getAttribute("t-call");
|
||||
if (tname in this.templates) {
|
||||
compiled.setAttribute("t-call", `{{this.templates[${toStringExpression(tname)}]}}`);
|
||||
compiled.setAttribute("t-call", `{{__comp__.templates[${toStringExpression(tname)}]}}`);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@ export class KanbanRecord extends Component {
|
||||
selection_mode: this.props.forceGlobalClick,
|
||||
user_context: this.user.context,
|
||||
widget: this.widget,
|
||||
__comp__: Object.assign(Object.create(this), { this: this }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
t-att-tabindex="props.record.model.useSampleModel ? -1 : 0"
|
||||
t-on-click="onGlobalClick"
|
||||
t-ref="root">
|
||||
<t t-call="{{ templates[this.constructor.KANBAN_BOX_ATTRIBUTE] }}" t-call-context="renderingContext"/>
|
||||
<t t-call="{{ templates[this.constructor.KANBAN_BOX_ATTRIBUTE] }}" t-call-context="this.renderingContext"/>
|
||||
<t t-call="{{ this.constructor.menuTemplate }}"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -223,7 +223,7 @@ export class ViewCompiler {
|
||||
{ selector: "widget", fn: this.compileWidget },
|
||||
];
|
||||
this.templates = templates;
|
||||
this.ctx = { readonly: "this.props.readonly" };
|
||||
this.ctx = { readonly: "__comp__.props.readonly" };
|
||||
|
||||
this.owlDirectiveRegexesWhitelist = this.constructor.OWL_DIRECTIVE_WHITELIST.map(
|
||||
(d) => new RegExp(d)
|
||||
@@ -246,8 +246,10 @@ export class ViewCompiler {
|
||||
if (typeof invisible === "boolean") {
|
||||
return;
|
||||
}
|
||||
const recordExpr = params.recordExpr || "this.props.record";
|
||||
let isVisileExpr = `!this.evalDomainFromRecord(${recordExpr},${JSON.stringify(invisible)})`;
|
||||
const recordExpr = params.recordExpr || "__comp__.props.record";
|
||||
let isVisileExpr = `!__comp__.evalDomainFromRecord(${recordExpr},${JSON.stringify(
|
||||
invisible
|
||||
)})`;
|
||||
if (compiled.hasAttribute("t-if")) {
|
||||
const formerTif = compiled.getAttribute("t-if");
|
||||
isVisileExpr = `( ${formerTif} ) and ${isVisileExpr}`;
|
||||
@@ -323,7 +325,7 @@ export class ViewCompiler {
|
||||
}
|
||||
const button = createElement("ViewButton", {
|
||||
tag: toStringExpression(tag),
|
||||
record: "this.props.record",
|
||||
record: "__comp__.props.record",
|
||||
});
|
||||
|
||||
assignOwlDirectives(button, el);
|
||||
@@ -382,8 +384,8 @@ export class ViewCompiler {
|
||||
const field = createElement("Field");
|
||||
field.setAttribute("id", `'${fieldId}'`);
|
||||
field.setAttribute("name", `'${fieldName}'`);
|
||||
field.setAttribute("record", params.recordExpr || "this.props.record");
|
||||
field.setAttribute("fieldInfo", `this.props.archInfo.fieldNodes['${fieldId}']`);
|
||||
field.setAttribute("record", params.recordExpr || "__comp__.props.record");
|
||||
field.setAttribute("fieldInfo", `__comp__.props.archInfo.fieldNodes['${fieldId}']`);
|
||||
|
||||
if (el.hasAttribute("widget")) {
|
||||
field.setAttribute("type", `'${el.getAttribute("widget")}'`);
|
||||
@@ -422,7 +424,7 @@ export class ViewCompiler {
|
||||
*/
|
||||
compileWidget(el) {
|
||||
const attrs = {};
|
||||
const props = { record: "this.props.record", readonly: this.ctx.readonly };
|
||||
const props = { record: "__comp__.props.record", readonly: this.ctx.readonly };
|
||||
for (const { name, value } of el.attributes) {
|
||||
switch (name) {
|
||||
case "class":
|
||||
|
||||
@@ -17,8 +17,11 @@ export class SettingsFormCompiler extends FormCompiler {
|
||||
|
||||
compileForm(el, params) {
|
||||
const settingsPage = createElement("SettingsPage");
|
||||
settingsPage.setAttribute("slots", "{NoContentHelper:this.props.slots.NoContentHelper}");
|
||||
settingsPage.setAttribute("initialTab", "this.props.initialApp");
|
||||
settingsPage.setAttribute(
|
||||
"slots",
|
||||
"{NoContentHelper:__comp__.props.slots.NoContentHelper}"
|
||||
);
|
||||
settingsPage.setAttribute("initialTab", "__comp__.props.initialApp");
|
||||
settingsPage.setAttribute("t-slot-scope", "settings");
|
||||
|
||||
//props
|
||||
@@ -91,7 +94,7 @@ export class SettingsFormCompiler extends FormCompiler {
|
||||
help: toStringExpression(el.getAttribute("help") || ""),
|
||||
companyDependent: el.getAttribute("company_dependent") === "1" || "false",
|
||||
documentation: toStringExpression(el.getAttribute("documentation") || ""),
|
||||
record: `this.props.record`,
|
||||
record: `__comp__.props.record`,
|
||||
});
|
||||
let string = toStringExpression(el.getAttribute("string") || "");
|
||||
let addLabel = true;
|
||||
|
||||
@@ -38,7 +38,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
const arch = /*xml*/ `<form><div>lol</div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div>lol</div>
|
||||
</div>
|
||||
</t>`;
|
||||
@@ -52,9 +52,9 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
const arch = /*xml*/ `<form><field name="test"/><label for="test" string=""/></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<Field id="'test'" name="'test'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['test']" />
|
||||
<FormLabel id="'test'" fieldName="'test'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['test']" className="""" string="\`\`" />
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" />
|
||||
<FormLabel id="'test'" fieldName="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" className="""" string="\`\`" />
|
||||
</div>
|
||||
</t>`;
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
@@ -65,10 +65,10 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
const arch = /*xml*/ `<form><div class="someClass">lol<field name="display_name"/></div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div class="someClass">
|
||||
lol
|
||||
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>`;
|
||||
@@ -88,15 +88,15 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
<OuterGroup>
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<InnerGroup class="scope && scope.className">
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'display_name',fieldName:'display_name',record:this.props.record,string:this.props.record.fields.display_name.string,fieldInfo:this.props.archInfo.fieldNodes['display_name']}" Component="this.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']" class="scope && scope.className" />
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'display_name',fieldName:'display_name',record:__comp__.props.record,string:__comp__.props.record.fields.display_name.string,fieldInfo:__comp__.props.archInfo.fieldNodes['display_name']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" class="scope && scope.className" />
|
||||
</t>
|
||||
</InnerGroup>
|
||||
</t>
|
||||
<t t-set-slot="item_1" type="'item'" sequence="1" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<InnerGroup class="scope && scope.className">
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'charfield',fieldName:'charfield',record:this.props.record,string:this.props.record.fields.charfield.string,fieldInfo:this.props.archInfo.fieldNodes['charfield']}" Component="this.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'charfield'" name="'charfield'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['charfield']" class="scope && scope.className" />
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" props="{id:'charfield',fieldName:'charfield',record:__comp__.props.record,string:__comp__.props.record.fields.charfield.string,fieldInfo:__comp__.props.archInfo.fieldNodes['charfield']}" Component="__comp__.constructor.components.FormLabel" subType="'item_component'" isVisible="true" itemSpan="2">
|
||||
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']" class="scope && scope.className" />
|
||||
</t>
|
||||
</InnerGroup>
|
||||
</t>
|
||||
@@ -120,13 +120,13 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
</form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<OuterGroup>
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<InnerGroup class="scope && scope.className">
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }} {{scope && scope.className || "" }}" class="o_form_nosheet">
|
||||
<div><Field id="'test'" name="'test'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['test']"/></div>
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }} {{scope && scope.className || "" }}" class="o_form_nosheet">
|
||||
<div><Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</InnerGroup>
|
||||
@@ -149,12 +149,12 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<Notebook defaultPage="this.props.record.isNew ? undefined : this.props.activeNotebookPages[0]" onPageUpdate="(page) => this.props.onNotebookPageChange(0, page)">
|
||||
<Notebook defaultPage="__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[0]" onPageUpdate="(page) => __comp__.props.onNotebookPageChange(0, page)">
|
||||
<t t-set-slot="page_1" title="\`Page1\`" name="\`p1\`" isVisible="true">
|
||||
<Field id="'charfield'" name="'charfield'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['charfield']"/>
|
||||
<Field id="'charfield'" name="'charfield'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['charfield']"/>
|
||||
</t>
|
||||
<t t-set-slot="page_2" title="\`Page2\`" name="\`p2\`" isVisible="true">
|
||||
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
|
||||
@@ -168,7 +168,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
</form>`;
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
|
||||
`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
@@ -183,7 +183,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
|
||||
<div>someDiv</div>
|
||||
</div>
|
||||
@@ -205,7 +205,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div t-att-class="this.props.class" t-attf-class="{{this.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ this.uiService.size < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ this.props.record.isDirty ? 'o_form_dirty' : !this.props.record.isVirtual ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ __comp__.uiService.size < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isVirtual ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<div class="o_form_sheet_bg">
|
||||
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
|
||||
<div>someDiv</div>
|
||||
@@ -239,7 +239,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<div class="visible3" />
|
||||
<div t-if="!this.evalDomainFromRecord(this.props.record,[["display_name","=","take"]])" />
|
||||
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,[["display_name","=","take"]])" />
|
||||
`;
|
||||
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
@@ -255,7 +255,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<div class="visible3" />
|
||||
<div t-if="!this.evalDomainFromRecord(this.props.record,"[['display_name','=','take']]")" />
|
||||
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,"[['display_name','=','take']]")" />
|
||||
`;
|
||||
assert.areContentEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
@@ -444,7 +444,7 @@ QUnit.module("Form Renderer", (hooks) => {
|
||||
|
||||
const arch = `<myNode modifiers="{"invisible": [["field", "=", "value"]]}" />`;
|
||||
|
||||
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !this.evalDomainFromRecord(this.props.record,[["field","=","value"]])" t-ref="compiled_view_root"/></t>`;
|
||||
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !__comp__.evalDomainFromRecord(__comp__.props.record,[["field","=","value"]])" t-ref="compiled_view_root"/></t>`;
|
||||
assert.areEquivalent(compileTemplate(arch), expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1457,11 +1457,11 @@ QUnit.module("SettingsFormView", (hooks) => {
|
||||
);
|
||||
|
||||
const expectedCompiled = `
|
||||
<SettingsPage slots="{NoContentHelper:this.props.slots.NoContentHelper}" initialTab="this.props.initialApp" t-slot-scope="settings" modules="[{"key":"crm","string":"CRM","imgurl":"/crm/static/description/icon.png"}]">
|
||||
<SettingsPage slots="{NoContentHelper:__comp__.props.slots.NoContentHelper}" initialTab="__comp__.props.initialApp" t-slot-scope="settings" modules="[{"key":"crm","string":"CRM","imgurl":"/crm/static/description/icon.png"}]">
|
||||
<SettingsApp key="\`crm\`" string="\`CRM\`" imgurl="\`/crm/static/description/icon.png\`" selectedTab="settings.selectedTab">
|
||||
<Setting title="\`\`" help="\`\`" companyDependent="false" documentation="\`\`" record="this.props.record" string="\`\`" addLabel="true" labels="["\`My\\" little ' Label\`"]">
|
||||
<FormLabel id="'display_name'" fieldName="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']" className=""highhopes"" string="\`My" little ' Label\`"/>
|
||||
<Field id="'display_name'" name="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']"/>
|
||||
<Setting title="\`\`" help="\`\`" companyDependent="false" documentation="\`\`" record="__comp__.props.record" string="\`\`" addLabel="true" labels="["\`My\\" little ' Label\`"]">
|
||||
<FormLabel id="'display_name'" fieldName="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" className=""highhopes"" string="\`My" little ' Label\`"/>
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
|
||||
</Setting>
|
||||
</SettingsApp>
|
||||
</SettingsPage>`;
|
||||
@@ -1502,7 +1502,7 @@ QUnit.module("SettingsFormView", (hooks) => {
|
||||
|
||||
const expectedCompiled = `
|
||||
<HighlightText originalText="\`this is Baz value: \`"/>
|
||||
<Field id="'baz'" name="'baz'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['baz']"/>
|
||||
<Field id="'baz'" name="'baz'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['baz']"/>
|
||||
<HighlightText originalText="\` and this is the after text\`"/>`;
|
||||
assert.areEquivalent(
|
||||
compiled.querySelector("Setting div.text-muted").innerHTML,
|
||||
|
||||
@@ -34,6 +34,12 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath', 'data-oe-source-id']
|
||||
|
||||
# Some views have a js compiler that generates an owl template from the arch. In that template,
|
||||
# `__comp__` is a reserved keyword giving access to the component instance (e.g. the form renderer
|
||||
# or the kanban record). However, we don't want to see implementation details leaking in archs, so
|
||||
# we use the following regex to detect the use of `__comp__` in dynamic attributes, to forbid it.
|
||||
COMP_REGEX = r'(^|[^\w])\s*__comp__\s*([^\w]|$)'
|
||||
|
||||
ref_re = re.compile(r"""
|
||||
# first match 'form_view_ref' key, backrefs are used to handle single or
|
||||
# double quoting of the value
|
||||
@@ -1826,6 +1832,8 @@ actual arch.
|
||||
|
||||
elif (attr.startswith("t-")):
|
||||
self._validate_qweb_directive(node, attr, node_info["view_type"])
|
||||
if (re.search(COMP_REGEX, expr)):
|
||||
self._raise_view_error(_("Forbidden use of `__comp__` in arch."), node)
|
||||
|
||||
def _validate_classes(self, node, expr):
|
||||
""" Validate the classes present on node. """
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import ast
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from functools import partial
|
||||
@@ -16,7 +17,7 @@ from odoo.exceptions import AccessError, ValidationError
|
||||
from odoo.tests import common, tagged
|
||||
from odoo.tools import get_cache_key_counter, mute_logger, view_validation
|
||||
from odoo.addons.base.models.ir_ui_view import (
|
||||
transfer_field_to_modifiers, transfer_node_to_modifiers, simplify_modifiers,
|
||||
transfer_field_to_modifiers, transfer_node_to_modifiers, simplify_modifiers, COMP_REGEX
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -3401,6 +3402,17 @@ Forbidden attribute used in arch (t-att-data-tooltip)."""
|
||||
Forbidden attribute used in arch (t-attf-data-tooltip-template)."""
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_ui_view')
|
||||
def test_forbidden_use_of___comp___in_kanban(self):
|
||||
arch = "<kanban><templates><t t-name='kanban-box'>%s</t></templates></kanban>"
|
||||
self.assertInvalid(
|
||||
arch % '<t t-esc="__comp__.props.resId"/>',
|
||||
"""Error while validating view near:
|
||||
|
||||
<kanban __validate__="1"><templates><t t-name="kanban-box"><t t-esc="__comp__.props.resId"/></t></templates></kanban>
|
||||
Forbidden use of `__comp__` in arch."""
|
||||
)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestDebugger(common.TransactionCase):
|
||||
@@ -4147,3 +4159,25 @@ class TestRenderAllViews(common.TransactionCase):
|
||||
|
||||
_logger.info('Rendered %d views as %s using (best of 5) %ss',
|
||||
count, self.env.user.name, elapsed)
|
||||
|
||||
|
||||
class CompRegexTest(common.TransactionCase):
|
||||
def test_comp_regex(self):
|
||||
self.assertIsNone(re.search(COMP_REGEX, ""))
|
||||
self.assertIsNone(re.search(COMP_REGEX, "__comp__2"))
|
||||
self.assertIsNone(re.search(COMP_REGEX, "__comp___that"))
|
||||
self.assertIsNone(re.search(COMP_REGEX, "a__comp__"))
|
||||
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ "))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, " __comp__ "))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__.props"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ .props"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__['props']"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ ['props']"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__[\"props\"]"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "__comp__ [\"props\"]"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, " __comp__ [\"props\"] "))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "record ? __comp__ : false"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "!__comp__.props.resId"))
|
||||
self.assertIsNotNone(re.search(COMP_REGEX, "{{ __comp__ }}"))
|
||||
|
||||
Reference in New Issue
Block a user