[FW][FIX] web: update owl from 2.0.2 to 2.0.3

Release notes: https://github.com/odoo/owl/releases/tag/v2.0.3

- fix: compiler: correctly escape backslashes when emitting block string
- fix: reactivity: don't subscribe to keys when making reactive
- fix: t-call-context: fix capture making component available in ctx
- fix: t-call-context: make `this` unavailable in rendering context

Description of the issue/feature this PR addresses:

Current behavior before PR:

Desired behavior after PR is merged:

closes odoo/odoo#110196

Forward-port-of: odoo/odoo#109976
Forward-port-of: odoo/odoo#109788
Related: odoo/enterprise#35939
Signed-off-by: Géry Debongnie <ged@odoo.com>
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Odoo's Mergebot
2023-01-18 10:16:13 +01:00
committed by GitHub
20 changed files with 180 additions and 126 deletions
@@ -3,7 +3,7 @@
<t t-name="analytic.AnalyticDistribution" owl="1">
<div class="o_field_tags d-inline-flex flex-wrap mw-100" t-att-class="{'o_tags_input o_input': !props.readonly}" t-ref="analyticDistribution" t-on-keydown="onWidgetKeydown">
<TagsList tags="tags"/>
<TagsList tags="this.tags"/>
<div t-if="!props.readonly" class="o_input_dropdown d-inline-flex w-100" tabindex="0" t-ref="mainElement" t-on-focus="onMainElementFocus" t-on-click="onMainElementFocus">
<span class="analytic_distribution_placeholder"/>
<a role="button" class="o_dropdown_button" draggable="false"/>
@@ -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">
@@ -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;
@@ -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>
+30 -28
View File
@@ -1759,7 +1759,7 @@
* @returns the raw type of the object
*/
function rawType(obj) {
return objectToString.call(obj).slice(8, -1);
return objectToString.call(toRaw(obj)).slice(8, -1);
}
/**
* Checks whether a given value can be made into a reactive object.
@@ -2309,7 +2309,8 @@
}
}
this.component = new C(props, env, this);
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
this.component.setup();
currentNode = null;
}
@@ -2500,6 +2501,7 @@
}
_patch() {
let hasChildren = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (let _k in this.children) {
hasChildren = true;
break;
@@ -2894,8 +2896,8 @@
}
return slotBDom || text("");
}
function capture(ctx, component) {
const result = ObjectCreate(component);
function capture(ctx) {
const result = ObjectCreate(ctx);
for (let k in ctx) {
result[k] = ctx[k];
}
@@ -2950,7 +2952,7 @@
class LazyValue {
constructor(fn, ctx, component, node, key) {
this.fn = fn;
this.ctx = capture(ctx, component);
this.ctx = capture(ctx);
this.component = component;
this.node = node;
this.key = key;
@@ -3110,7 +3112,7 @@
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
@@ -3232,7 +3234,7 @@
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date".split(",");
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
and: "&&",
or: "||",
@@ -3710,7 +3712,7 @@
for (let block of this.blocks) {
if (block.dom) {
let xmlString = block.asXmlString();
xmlString = xmlString.replace(/`/g, "\\`");
xmlString = xmlString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
if (block.dynamicTagName) {
xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
@@ -3817,16 +3819,16 @@
const mapping = new Map();
return tokens
.map((tok) => {
if (tok.varName && !tok.isLocal) {
if (!mapping.has(tok.varName)) {
const varId = generateId("v");
mapping.set(tok.varName, varId);
this.define(varId, tok.value);
}
tok.value = mapping.get(tok.varName);
if (tok.varName && !tok.isLocal) {
if (!mapping.has(tok.varName)) {
const varId = generateId("v");
mapping.set(tok.varName, varId);
this.define(varId, tok.value);
}
return tok.value;
})
tok.value = mapping.get(tok.varName);
}
return tok.value;
})
.join("");
}
/**
@@ -3927,11 +3929,11 @@
.split(".")
.slice(1)
.map((m) => {
if (!MODS.has(m)) {
throw new OwlError(`Unknown event modifier: '${m}'`);
}
return `"${m}"`;
});
if (!MODS.has(m)) {
throw new OwlError(`Unknown event modifier: '${m}'`);
}
return `"${m}"`;
});
let modifiersCode = "";
if (modifiers.length) {
modifiersCode = `${modifiers.join(",")}, `;
@@ -4508,7 +4510,7 @@
if (this.target.loopLevel || !this.hasSafeContext) {
ctxStr = generateId("ctx");
this.helpers.add("capture");
this.define(ctxStr, `capture(ctx, this)`);
this.define(ctxStr, `capture(ctx)`);
}
let slotStr = [];
for (let slotName in ast.slots) {
@@ -4667,7 +4669,7 @@
if (this.target.loopLevel || !this.hasSafeContext) {
ctxStr = generateId("ctx");
this.helpers.add("capture");
this.define(ctxStr, `capture(ctx, this)`);
this.define(ctxStr, `capture(ctx)`);
}
let id = generateId("comp");
this.staticDefs.push({
@@ -5412,7 +5414,7 @@
if (line[columnIndex]) {
msg +=
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
`${line}\n${"-".repeat(columnIndex - 1)}^`;
`${line}\n${"-".repeat(columnIndex - 1)}^`;
}
}
}
@@ -5856,9 +5858,9 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
Object.defineProperty(exports, '__esModule', { value: true });
__info__.version = '2.0.2';
__info__.date = '2022-11-29T14:11:21.188Z';
__info__.hash = 'ef8baa2';
__info__.version = '2.0.3';
__info__.date = '2023-01-12T15:32:52.837Z';
__info__.hash = '316eb06';
__info__.url = 'https://github.com/odoo/owl';
@@ -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>
+9 -7
View File
@@ -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="&quot;&quot;" 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="&quot;&quot;" 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; scope.className || &quot;&quot; }}" 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 &amp;&amp; scope.className || &quot;&quot; }}" 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) =&gt; this.props.onNotebookPageChange(0, page)">
<Notebook defaultPage="__comp__.props.record.isNew ? undefined : __comp__.props.activeNotebookPages[0]" onPageUpdate="(page) =&gt; __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 &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ 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 &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ __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,[[&quot;display_name&quot;,&quot;=&quot;,&quot;take&quot;]])" />
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,[[&quot;display_name&quot;,&quot;=&quot;,&quot;take&quot;]])" />
`;
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,&quot;[['display_name','=','take']]&quot;)" />
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,&quot;[['display_name','=','take']]&quot;)" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
});
@@ -444,7 +444,7 @@ QUnit.module("Form Renderer", (hooks) => {
const arch = `<myNode modifiers="{&quot;invisible&quot;: [[&quot;field&quot;, &quot;=&quot;, &quot;value&quot;]]}" />`;
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !this.evalDomainFromRecord(this.props.record,[[&quot;field&quot;,&quot;=&quot;,&quot;value&quot;]])" t-ref="compiled_view_root"/></t>`;
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !__comp__.evalDomainFromRecord(__comp__.props.record,[[&quot;field&quot;,&quot;=&quot;,&quot;value&quot;]])" 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="[{&quot;key&quot;:&quot;crm&quot;,&quot;string&quot;:&quot;CRM&quot;,&quot;imgurl&quot;:&quot;/crm/static/description/icon.png&quot;}]">
<SettingsPage slots="{NoContentHelper:__comp__.props.slots.NoContentHelper}" initialTab="__comp__.props.initialApp" t-slot-scope="settings" modules="[{&quot;key&quot;:&quot;crm&quot;,&quot;string&quot;:&quot;CRM&quot;,&quot;imgurl&quot;:&quot;/crm/static/description/icon.png&quot;}]">
<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="[&quot;\`My\\&quot; little ' Label\`&quot;]">
<FormLabel id="'display_name'" fieldName="'display_name'" record="this.props.record" fieldInfo="this.props.archInfo.fieldNodes['display_name']" className="&quot;highhopes&quot;" string="\`My&quot; 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="[&quot;\`My\\&quot; little ' Label\`&quot;]">
<FormLabel id="'display_name'" fieldName="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" className="&quot;highhopes&quot;" string="\`My&quot; 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,
+8
View File
@@ -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. """
+35 -1
View File
@@ -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__ }}"))