forked from odoo/odoo
/
ir_qweb.py
2628 lines (2200 loc) · 114 KB
/
ir_qweb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
================
IrQWeb / ir.qweb
================
Preamble
========
Technical documentation of the python operation of the rendering QWeb engine.
Templating
==========
QWeb is the primary templating engine used by Odoo. It is an XML templating
engine and used mostly to generate XML, HTML fragments and pages.
Template directives are specified as XML attributes prefixed with ``t-``,
for instance ``t-if`` for :ref:`reference/qweb/conditionals`, with elements
and other attributes being rendered directly.
To avoid element rendering, a placeholder element ``<t>`` is also available,
which executes its directive but doesn't generate any output in and of
itself.
To create new XML template, please see :doc:`QWeb Templates documentation
<https://www.odoo.com/documentation/16.0/developer/reference/frontend/qweb.html>`
Rendering process
=================
In **input** you have an XML template giving the corresponding input etree.
Each etree input nodes are used to generate a python function. This fonction is
called and will give the XML **output**.
The ``_compile`` method is responsible to generate the function from the
etree, that function is a python generator that yield one output line at a
time. This generator is consumed by ``_render``. The generated function is orm
cached.
For performance, the **compile time** (when input, XML template or template
id, is compiled into a function) is less important than the **rendering time**
(when the function is called with the different values). The generation of the
function is only done once (for a set of options, language, branding ...)
because it is cached orm
The output is in ``MarkupSafe`` format. ``MarkupSafe`` escapes characters so
text is safe to use in HTML and XML. Characters that have special meanings
are replaced so that they display as the actual characters. This mitigates
injection attacks, meaning untrusted user input can safely be displayed on a
page.
At **compile time**, each dynamic attribute ``t-*`` will be compiled into
specific python code. (For example ``<t t-out="5 + 5"/>`` will insert the
template "10" inside the output)
At **compile time**, each directive removes the dynamic attribute it uses from
the input node attributes. At the end of the compilation each input node, no
dynamic attributes must remain.
How the code works
==================
In the graphic below you can see theresume of the call of the methods performed
in the IrQweb class.
.. code-block:: rst
Odoo
┗━► _render (returns MarkupSafe)
┗━► _compile (returns function) ◄━━━━━━━━━━┓
┗━► _compile_node (returns code string array) ◄━━━━━━━━┓ ┃
┃ (skip the current node if found t-qweb-skip) ┃ ┃
┃ (add technical directives: t-tag-open, t-tag-close, t-inner-content) ┃ ┃
┃ ┃ ┃
┣━► _directives_eval_order (defined directive order) ┃ ┃
┣━► _compile_directives (loop) Consume all remaining directives ◄━━━┓ ┃ ┃
┃ ┃ (e.g.: to change the indentation) ┃ ┃ ┃
┃ ┣━► _compile_directive ┃ ┃ ┃
┃ ┃ ┗━► t-nocache ━━► _compile_directive_nocache ━┫ ┃ ┃
┃ ┃ ┗━► t-cache ━━► _compile_directive_cache ━┫ ┃ ┃
┃ ┃ ┗━► t-groups ━━► _compile_directive_groups ━┫ ┃ ┃
┃ ┃ ┗━► t-foreach ━━► _compile_directive_foreach ━┫ ┃ ┃
┃ ┃ ┗━► t-if ━━► _compile_directive_if ━┛ ┃ ┃
┃ ┃ ┗━► t-inner-content ━━► _compile_directive_inner_content ◄━━━━━┓ ━┛ ┃
┃ ┃ ┗━► t-options ━━► _compile_directive_options ┃ ┃
┃ ┃ ┗━► t-set ━━► _compile_directive_set ◄━━┓ ┃ ┃
┃ ┃ ┗━► t-call ━━► _compile_directive_call ━┛ ━┫ ━━━┛
┃ ┃ ┗━► t-att ━━► _compile_directive_att ┃
┃ ┃ ┗━► t-tag-open ━━► _compile_directive_open ◄━━┓ ┃
┃ ┃ ┗━► t-tag-close ━━► _compile_directive_close ◄━━┫ ┃
┃ ┃ ┗━► t-out ━━► _compile_directive_out ━┛ ━┫ ◄━━┓
┃ ┃ ┗━► t-field ━━► _compile_directive_field ┃ ━┫
┃ ┃ ┗━► t-esc ━━► _compile_directive_esc ┃ ━┛
┃ ┃ ┗━► t-* ━━► ... ┃
┃ ┃ ┃
┗━━┻━► _compile_static_node ━┛
The QWeb ``_render`` uses the function generated by the ``_compile`` method.
Each XML node will go through the ``_compile_node`` method. If the
node does not have dynamic directives or attributes (``_is_static_node``).
A ``static`` is a node without ``t-*`` attributes, does not require dynamic
rendering for its attributes.
If it's a ``static`` node, the ``_compile_static_node`` method is called,
otherwise it is the ``_compile_directives`` method after having prepared the
order for calling the directives using the ``_directives_eval_order`` method.
In the defined order, for each directive the method ``_compile_directive`` is
called which itself dispatches to the methods corresponding to the directives
``_compile_directive_[name of the directive]`` (for example: ``t-if`` =>
``_compile_directive_if``). After all ordered directives, the directives
attributes still present on the element are compiled.
The ``_post_processing_att`` method is used for the generation of rendering
attributes. If the attributes come from static XML template nodes then the
method is called only once when generating the render function. Otherwise the
method is called during each rendering.
Each expression is compiled by the method ``_compile_expr`` into a python
expression whose values are namespaced.
Directives
----------
``t-debug``
~~~~~~~~~~~
**Values**: ``pdb``, ``ipdb``, ``pudb``, ``wdb``
Activate the choosed debugger.
When dev mode is enabled this allows python developers to have access to the
state of variables being rendered. The code generated by the QWeb engine is
not accessible, only the variables (values, self) can be analyzed or the
methods that called the QWeb rendering.
``t-if``
~~~~~~~~
**Values**: python expression
Add an python ``if`` condition to the code string array, and call
``_compile_directives`` to level and add the code string array corresponding
to the other directives and content.
The structure of the dom is checked to possibly find a ``t-else`` or
``t-elif``. If these directives exist then the compilation is performed and
the nodes are marked not to be rendered twice.
At **rendering time** the other directives code and content will used only if
the expression is evaluated as truely.
The ``t-else``, ``t-elif`` and ``t-if`` are not compiled at the same time like
defined in ``_directives_eval_order`` method.
```
<t t-set="check" t-value="1"/>
<section t-if="False">10</section>
<span t-elif="check == 1" t-foreach="range(3)" t-as="check" t-esc="check"/>
<section t-if="False">10</section>
<div t-else="" t-if="check == 1" t-foreach="range(3)" t-as="check" t-esc="check"/>
Result:
<span>0</span>
<span>1</span>
<span>2</span>
<div>1</div>
```
``t-else``
~~~~~~~~~~
**Values**: nothing
Only validate the **input**, the compilation if inside the ``t-if`` directive.
``t-elif``
~~~~~~~~~~
**Values**: python expression
Only validate the **input**, the compilation if inside the ``t-if`` directive.
``t-groups`` (``groups`` is an alias)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Values**: name of the allowed odoo user group, or preceded by ``!`` for
prohibited groups
The generated code uses ``user_has_groups`` Odoo method.
``t-foreach``
~~~~~~~~~~~~~
**Values**: an expression returning the collection to iterate on
This directive is used with ``t-as`` directive to defined the key name. The
directive will be converted into a ``for`` loop. In this loop, different values
are added to the dict (``values`` in the generated method) in addition to the
key defined by ``t-name``, these are (``*_value``, ``*_index``, ``*_size``,
``*_first``, ``*_last``).
``t-as``
~~~~~~~~
**Values**: key name
The compilation method only validates if ``t-as`` and ``t-foreach`` are on the
same node.
``t-options`` and ``t-options-*``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Values**: python expression
It's use on the same node of another directive, it's used to configure the
other directive. Used on the same ``input node`` of the directives ``t-call``,
``t-field`` or ``t-out``.
Create a ``values['__qweb_options__']`` dict from the optional ``t-options``
expression and add each key-value ``t-options-key="expression value"`` to this
dict. (for example: ``t-options="{'widget': 'float'}"`` is equal to
``t-options-widget="'float'"``)
``t-att``, ``t-att-*`` and ``t-attf-*``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Values**: python expression (or format string expression for ``t-attf-``)
Compile the attributes to create ``values['__qweb_attrs__']`` dictionary code
in the compiled function. Use the ``t-att`` expression and add each key-value
``t-att-key="expression value"`` to this dict. (for example:
``t-att="{'class': f'float_{1}'}"`` is equal to ``t-att-class="f'float_{1}'"``
and is equal to ``t-attf-class="float_{{1}}")
The attributes come from new namespaces, static elements (not preceded
by ``t-``) and dynamic attributes ``t-att``, attributes prefixed by ``t-att-``
(python expression) or ``t-attf`` (format string expression).
``t-call``
~~~~~~~~~~
**Values**: format string expression for template name
Serves the called template in place of the current ``t-call`` node.
Here are the different steps performed by the generated python code:
#. copy the ``values`` dictionary;
#. render the content (``_compile_directive_inner_content``) of the tag in a
separate method called with the previous copied values. This values can be
updated via t-set. The visible content of the rendering of the sub-content
is added as a magical value ``0`` (can be rendered with ``t-out="0"``);
#. copy the ``compile_context`` dictionary;
#. compile the directive ``t-options`` and update the ``compile_context``
are, in added to the calling template and the ``nsmap`` values;
#. get the compiled function from the ``_compile`` method;
#. use the compiled function to serves the called template.
``t-lang``
~~~~~~~~~~
**Values**: python expression
Used to serve the called template (``t-call``) in another language. Used
together with ``t-call``.
This directive will be evaluate like ``t-options-lang``. Allows you to change
the language in which the called template is rendered. It's in the ``t-call``
directive that the language of the context of the ``ir.qweb`` recordset on
which the ``_compile`` function is called is updated.
``t-call-assets``
~~~~~~~~~~~~~~~~~
**Values**: format string for template name
The generated code call the ``_get_asset_nodes`` method to get the list of
(tagName, attrs and content). From each tuple a tag is created into the
rendering.
``t-out``
~~~~~~~~~
**Values**: python expression
Output the given value or if falsy, display the content as default value.
(for example: ``<t t-out="given_value">Default content</t>``)
The generated code add the value into the ``MarkupSafe`` rendering.
If a widget is defined (``t-options-widget``), the generated code call the
``_get_widget`` method to have the formatted field value and attributes. It's
the ``ir.qweb.field.*`` models that format the value.
``t-field``
~~~~~~~~~~~
**Values**: String representing the path to the field. (for example:
``t-field="record.name"``)
Output the field value or if falsy, display the content as default value.
(for example: ``<span t-field="record.name">Default content</span>``)
Use ``t-out`` compile method but the generated code call ``_get_field``
instead of ``_get_widget``. It's the ``ir.qweb.field.*`` models that format
the value. The rendering model is chosen according to the type of field. The
rendering model can be modified via the ``t-options-widget``.
``t-esc``
~~~~~~~~~
Deprecated, please use ``t-out``
``t-raw``
~~~~~~~~~
Deprecated, please use ``t-out``
``t-set``
~~~~~~~~~
**Values**: key name
The generated code update the key ``values`` dictionary equal to the value
defined by ``t-value`` expression, ``t-valuef`` format string expression or
to the ``MarkupSafe`` rendering come from the content of the node.
``t-value``
~~~~~~~~~~~
**Values**: python expression
The compilation method only validates if ``t-value`` and ``t-set`` are on the
same node.
``t-valuef``
~~~~~~~~~~~~
**Values**: format string expression
The compilation method only validates if ``t-valuef`` and ``t-set`` are on the
same node.
Technical directives
--------------------
Directive added automatically by IrQweb in order to go through the compilation
methods.
``t-tag-open``
~~~~~~~~~~~~~~
Used to generate the opening HTML/XML tags.
``t-tag-close``
~~~~~~~~~~~~~~
Used to generate the closing HTML/XML tags.
``t-inner-content``
~~~~~~~~~~~~~~~~~~~
Used to add the content of the node (text, tail and children nodes).
If namespaces are declared on the current element then a copy of the options
is made.
``t-consumed-options``
~~~~~~~~~~~~~~~~~~~~~~
Raise an exception if the ``t-options`` is not consumed.
``t-qweb-skip``
~~~~~~~~~~~~~~~~~~~~~~
Ignore rendering and directives for the curent **input** node.
``t-else-valid``
~~~~~~~~~~~~~~~~~~~~~~
Mark a node with ``t-else`` or ``t-elif`` having a valid **input** dom
structure.
"""
import fnmatch
import io
import logging
import math
import re
import textwrap
import time
import token
import tokenize
import traceback
import werkzeug
from markupsafe import Markup, escape
from collections.abc import Sized, Mapping
from itertools import count, chain
from lxml import etree
from dateutil.relativedelta import relativedelta
from psycopg2.extensions import TransactionRollbackError
from odoo import api, models, tools
from odoo.tools import config, safe_eval, pycompat, SUPPORTED_DEBUGGER
from odoo.tools.safe_eval import assert_valid_codeobj, _BUILTINS, to_opcodes, _EXPR_OPCODES, _BLACKLIST
from odoo.tools.json import scriptsafe
from odoo.tools.misc import str2bool
from odoo.tools.image import image_data_uri
from odoo.http import request
from odoo.modules.module import get_resource_path, get_module_path
from odoo.tools.profiler import QwebTracker
from odoo.exceptions import UserError, AccessDenied, AccessError, MissingError, ValidationError
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS, TEMPLATE_EXTENSIONS
_logger = logging.getLogger(__name__)
# QWeb token usefull for generate expression used in `_compile_expr_tokens` method
token.QWEB = token.NT_OFFSET - 1
token.tok_name[token.QWEB] = 'QWEB'
# security safe eval opcodes for generated expression validation, used in `_compile_expr`
_SAFE_QWEB_OPCODES = _EXPR_OPCODES.union(to_opcodes([
'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX',
'CALL_METHOD', 'LOAD_METHOD',
'GET_ITER', 'FOR_ITER', 'YIELD_VALUE',
'JUMP_FORWARD', 'JUMP_ABSOLUTE',
'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE', 'POP_JUMP_IF_TRUE',
'LOAD_NAME', 'LOAD_ATTR',
'LOAD_FAST', 'STORE_FAST', 'UNPACK_SEQUENCE',
'STORE_SUBSCR',
'LOAD_GLOBAL',
])) - _BLACKLIST
# eval to compile generated string python code into binary code, used in `_compile`
unsafe_eval = eval
VOID_ELEMENTS = frozenset([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
# Terms allowed in addition to AVAILABLE_OBJECTS when compiling python expressions
ALLOWED_KEYWORD = frozenset(['False', 'None', 'True', 'and', 'as', 'elif', 'else', 'for', 'if', 'in', 'is', 'not', 'or'] + list(_BUILTINS))
# regexpr for string formatting and extract ( ruby-style )|( jinja-style ) used in `_compile_format`
FORMAT_REGEX = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
RSTRIP_REGEXP = re.compile(r'\n[ \t]*$')
FIRST_RSTRIP_REGEXP = re.compile(r'^(\n[ \t]*)+(\n[ \t])')
VARNAME_REGEXP = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
TO_VARNAME_REGEXP = re.compile(r'[^A-Za-z0-9_]+')
# Attribute name used outside the context of the QWeb.
SPECIAL_DIRECTIVES = {'t-translation', 't-ignore', 't-title'}
# Name of the variable to insert the content in t-call in the template.
# The slot will be replaced by the `t-call` tag content of the caller.
T_CALL_SLOT = '0'
def indent_code(code, level):
"""Indent the code to respect the python syntax."""
return textwrap.indent(textwrap.dedent(code).strip(), ' ' * 4 * level)
def keep_query(*keep_params, **additional_params):
"""
Generate a query string keeping the current request querystring's parameters specified
in ``keep_params`` and also adds the parameters specified in ``additional_params``.
Multiple values query string params will be merged into a single one with comma seperated
values.
The ``keep_params`` arguments can use wildcards too, eg:
keep_query('search', 'shop_*', page=4)
"""
if not keep_params and not additional_params:
keep_params = ('*',)
params = additional_params.copy()
qs_keys = list(request.httprequest.args) if request else []
for keep_param in keep_params:
for param in fnmatch.filter(qs_keys, keep_param):
if param not in additional_params and param in qs_keys:
params[param] = request.httprequest.args.getlist(param)
return werkzeug.urls.url_encode(params)
####################################
### QWebException ###
####################################
class QWebException(Exception):
""" Management of errors that raised when rendering a QWeb template.
"""
def __init__(self, message, qweb, template=None, ref=None, path_xml=None, code=None):
self.stack = traceback.format_exc()
self.name = template
self.ref = ref
self.path, self.html = path_xml or (None, None)
self.code = None
if code:
self.code = '\n'.join(code.split('\n')[:-1]) if qweb.env.context.get('dev_mode') else None
line_nb = 0
for error_line in reversed(self.stack.split('\n')):
if f'File "<{self.ref}>"' in error_line:
line_function = error_line.split(', line ')[1]
line_nb = int(line_function.split(',')[0])
break
for code_line in reversed(code.split('\n')[:line_nb]):
match = re.match(r'\s*# element: (.*) , (.*)', code_line)
if match:
self.path = match[1][1:-1]
self.html = match[2][1:-1]
break
self.title = message
super().__init__(message)
def __str__(self):
parts = [self.title]
if self.__cause__ and str(self.__cause__) != '':
parts.append(f"{self.__cause__.__class__.__name__}: {self.__cause__}")
elif self.__context__ and str(self.__context__) != '':
parts.append(f"{self.__context__.__class__.__name__}: {self.__context__}")
if self.name is not None:
parts.append(f"Template: {self.name}")
if self.path is not None:
parts.append(f"Path: {self.path}")
if self.html is not None:
parts.append(f"Node: {self.html}")
if self.code is not None:
parts.append(f"Compiled code:\n{self.code}")
return "\n".join(parts)
def __repr__(self):
return f"QWebException({self.title!r})"
####################################
### QWeb ###
####################################
class IrQWeb(models.AbstractModel):
""" Base QWeb rendering engine
* to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
create new models called :samp:`ir.qweb.field.{widget}`
Beware that if you need extensions or alterations which could be
incompatible with other subsystems, you should create a local object
inheriting from ``ir.qweb`` and customize that.
"""
_name = 'ir.qweb'
_description = 'Qweb'
@QwebTracker.wrap_render
@api.model
def _render(self, template, values=None, **options):
""" render(template, values, **options)
Render the template specified by the given name.
:param template: etree, xml_id, template name (see _get_template)
* Call the method ``load`` is not an etree.
:param dict values: template values to be used for rendering
:param options: used to compile the template
Options will be add into the IrQweb.env.context for the rendering.
* ``lang`` (str) used language to render the template
* ``inherit_branding`` (bool) add the tag node branding
* ``inherit_branding_auto`` (bool) add the branding on fields
* ``minimal_qcontext``(bool) To use the minimum context and options
from ``_prepare_environment``
:returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
instead of `str`)
:rtype: MarkupSafe
"""
values = values.copy() if values else {}
if T_CALL_SLOT in values:
raise ValueError(f'values[{T_CALL_SLOT}] should be unset when call the _render method and only set into the template.')
irQweb = self.with_context(**options)._prepare_environment(values)
safe_eval.check_values(values)
template_functions, def_name = irQweb._compile(template)
render_template = template_functions[def_name]
rendering = render_template(irQweb, values)
result = ''.join(rendering)
return Markup(result)
# assume cache will be invalidated by third party on write to ir.ui.view
def _get_template_cache_keys(self):
""" Return the list of context keys to use for caching ``_compile``. """
return ['lang', 'inherit_branding', 'edit_translations', 'profile']
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
)
def _get_view_id(self, template):
try:
return self.env['ir.ui.view'].sudo().with_context(load_all_views=True)._get_view_id(template)
except Exception:
return None
@QwebTracker.wrap_compile
def _compile(self, template):
if isinstance(template, etree._Element):
self = self.with_context(is_t_cache_disabled=True)
ref = None
else:
ref = self._get_view_id(template)
# define the base key cache for code in cache and t-cache feature
base_key_cache = None
if ref:
base_key_cache = self._get_cache_key(tuple([ref] + [self.env.context.get(k) for k in self._get_template_cache_keys()]))
self = self.with_context(__qweb_base_key_cache=base_key_cache)
# generate the template functions and the root function name
def generate_functions():
code, options, def_name = self._generate_code(template)
code = '\n'.join([
"def generate_functions():",
" template_functions = {}",
indent_code(code, 1),
f" template_functions['options'] = {options if self.env.context.get('profile') else None!r}",
" return template_functions",
])
try:
compiled = compile(code, f"<{ref}>", 'exec')
globals_dict = self._prepare_globals()
globals_dict['__builtins__'] = globals_dict # So that unknown/unsafe builtins are never added.
unsafe_eval(compiled, globals_dict)
return globals_dict['generate_functions'](), def_name
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
self, template, code=code, ref=ref) from e
return self._load_values(base_key_cache, generate_functions)
def _generate_code(self, template):
""" Compile the given template into a rendering function (generator)::
render_template(qweb, values)
This method can be called only by the IrQweb `_render` method or by
the compiled code of t-call from an other template.
An `options` dictionary is created and attached to the function. It
contains rendering options that are part of the cache key in
addition to template references.
where ``qweb`` is a QWeb instance and ``values`` are the values to
render.
:returns: tuple containing code, options and main method name
"""
# The `compile_context`` dictionary includes the elements used for the
# cache key to which are added the template references as well as
# technical information useful for generating the function. This
# dictionary is only used when compiling the template.
compile_context = self.env.context.copy()
try:
element, document, ref = self._get_template(template)
except (ValueError, UserError) as e:
# return the error function if the template is not found or fail
message = str(e)
code = indent_code(f"""
def not_found_template(self, values):
if self.env.context.get('raise_if_not_found', True):
raise {e.__class__.__name__}({message!r})
warning('Cannot load template %s: %s', {template!r}, {message!r})
return ''
template_functions = {{'not_found_template': not_found_template}}
""", 0)
return (code, {}, 'not_found_template')
compile_context.pop('raise_if_not_found', None)
# reference to get xml and etree (usually the template ID)
compile_context['ref'] = ref
# reference name or key to get xml and etree (usually the template XML ID)
compile_context['ref_name'] = element.attrib.pop('t-name', template if isinstance(template, str) and '<' not in template else None)
# str xml of the reference template used for compilation. Useful for debugging, dev mode and profiling.
compile_context['ref_xml'] = document
# Identifier used to call `_compile`
compile_context['template'] = template
# Root of the etree which will be processed during compilation.
compile_context['root'] = element.getroottree()
# Reference to the last node being compiled. It is mainly used for debugging and displaying error messages.
compile_context['_qweb_error_path_xml'] = None
if not compile_context.get('nsmap'):
compile_context['nsmap'] = {}
# The options dictionary includes cache key elements and template
# references. It will be attached to the generated function. This
# dictionary is only there for logs, performance or test information.
# The values of these `options` cannot be changed and must always be
# identical in `context` and `self.env.context`.
options = {k: compile_context.get(k) for k in self._get_template_cache_keys() + ['ref', 'ref_name', 'ref_xml']}
# generate code
def_name = TO_VARNAME_REGEXP.sub(r'_', f'template_{ref}')
name_gen = count()
compile_context['make_name'] = lambda prefix: f"{def_name}_{prefix}_{next(name_gen)}"
try:
if element.text:
element.text = FIRST_RSTRIP_REGEXP.sub(r'\2', element.text)
compile_context['template_functions'] = {}
compile_context['_text_concat'] = []
self._append_text("", compile_context) # To ensure the template function is a generator and doesn't become a regular function
compile_context['template_functions'][f'{def_name}_content'] = (
[f"def {def_name}_content(self, values):"]
+ self._compile_node(element, compile_context, 2)
+ self._flush_text(compile_context, 2, rstrip=True))
compile_context['template_functions'][def_name] = [indent_code(f"""
def {def_name}(self, values):
try:
if '__qweb_loaded_values' not in values:
values['__qweb_loaded_values'] = {{}}
values['__qweb_root_values'] = values.copy()
values['xmlid'] = {options['ref_name']!r}
values['viewid'] = {options['ref']!r}
values['__qweb_loaded_values'].update(template_functions)
yield from {def_name}_content(self, values)
except QWebException:
raise
except Exception as e:
if isinstance(e, TransactionRollbackError):
raise
raise QWebException("Error while render the template",
self, template, ref={compile_context['ref']!r}, code=code) from e
""", 0)]
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
self, template, ref=compile_context['ref'], path_xml=compile_context['_qweb_error_path_xml']) from e
code_lines = ['code = None']
code_lines.append(f'template = {(document if isinstance(template, etree._Element) else template)!r}')
code_lines.append('template_functions = {}')
for lines in compile_context['template_functions'].values():
code_lines.extend(lines)
for name in compile_context['template_functions']:
code_lines.append(f'template_functions[{name!r}] = {name}')
code = '\n'.join(code_lines)
code += f'\n\ncode = {code!r}'
return (code, options, def_name)
# read and load input template
def _get_template(self, template):
""" Retrieve the given template, and return it as a tuple ``(etree,
xml, ref)``, where ``element`` is an etree, ``document`` is the
string document that contains ``element``, and ``ref`` if the uniq
reference of the template (id, t-name or template).
:param template: template identifier or etree
"""
assert template not in (False, None, ""), "template is required"
# template is an xml etree already
if isinstance(template, etree._Element):
element = template
document = etree.tostring(template, encoding='unicode')
ref = None
# template is xml as string
elif isinstance(template, str) and '<' in template:
raise ValueError('Inline templates must be passed as `etree` documents')
# template is (id or ref) to a database stored template
else:
try:
ref_alias = int(template) # e.g. <t t-call="33"/>
except ValueError:
ref_alias = template # e.g. web.layout
doc_or_elem, ref = self._load(ref_alias) or (None, None)
if doc_or_elem is None:
raise ValueError(f"Can not load template: {ref_alias!r}")
if isinstance(doc_or_elem, etree._Element):
element = doc_or_elem
document = etree.tostring(doc_or_elem, encoding='unicode')
elif isinstance(doc_or_elem, str):
element = etree.fromstring(doc_or_elem)
document = doc_or_elem
else:
raise TypeError(f"Loaded template {ref!r} should be a string.")
# return etree, document and ref, or try to find the ref
if ref:
return (element, document, ref)
# <templates>
# <template t-name=... /> <!-- return ONLY this element -->
# <template t-name=... />
# </templates>
for node in element.iter():
ref = node.get('t-name')
if ref:
return (node, document, ref)
# use the document itself as ref when no t-name was found
return (element, document, document)
def _load(self, ref):
"""
Load the template referenced by ``ref``.
:returns: The loaded template (as string or etree) and its
identifier
:rtype: Tuple[Union[etree, str], Optional[str, int]]
"""
IrUIView = self.env['ir.ui.view'].sudo()
view = IrUIView._get(ref)
template = IrUIView._read_template(view.id)
etree_view = etree.fromstring(template)
xmlid = view.key or ref
if isinstance(ref, int):
domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', view.id)]
model_data = self.env['ir.model.data'].sudo().search_read(domain, ['module', 'name'], limit=1)
if model_data:
xmlid = f"{model_data[0]['module']}.{model_data[0]['name']}"
# QWeb's ``_read_template`` will check if one of the first children of
# what we send to it has a "t-name" attribute having ``ref`` as value
# to consider it has found it. As it'll never be the case when working
# with view ids or children view or children primary views, force it here.
if view.inherit_id is not None:
for node in etree_view:
if node.get('t-name') == str(ref) or node.get('t-name') == str(view.key):
node.attrib.pop('name', None)
node.attrib.pop('id', None)
etree_view = node
break
etree_view.set('t-name', str(xmlid))
return (etree_view, view.id)
# values for running time
def _prepare_environment(self, values):
""" Prepare the values and context that will sent to the
compiled and evaluated function.
:param values: template values to be used for rendering
:returns self (with new context)
"""
debug = request and request.session.debug or ''
values.update(
true=True,
false=False,
)
if not self.env.context.get('minimal_qcontext'):
values.setdefault('debug', debug)
values.setdefault('user_id', self.env.user.with_env(self.env))
values.setdefault('res_company', self.env.company.sudo())
values.update(
request=request, # might be unbound if we're not in an httprequest context
test_mode_enabled=bool(config['test_enable'] or config['test_file']),
json=scriptsafe,
quote_plus=werkzeug.urls.url_quote_plus,
time=safe_eval.time,
datetime=safe_eval.datetime,
relativedelta=relativedelta,
image_data_uri=image_data_uri,
# specific 'math' functions to ease rounding in templates and lessen controller marshmalling
floor=math.floor,
ceil=math.ceil,
env=self.env,
lang=self.env.context.get('lang'),
keep_query=keep_query,
)
context = {'dev_mode': 'qweb' in tools.config['dev_mode']}
if 'xml' in tools.config['dev_mode']:
context['is_t_cache_disabled'] = True
elif 'disable-t-cache' in debug:
context['is_t_cache_disabled'] = True
return self.with_context(**context)
def _prepare_globals(self):
""" Prepare the global context that will sent to eval the qweb
generated code.
"""
return {
'Sized': Sized,
'Mapping': Mapping,
'Markup': Markup,
'escape': escape,
'VOID_ELEMENTS': VOID_ELEMENTS,
'QWebException': QWebException,
'Exception': Exception,
'TransactionRollbackError': TransactionRollbackError, # for SerializationFailure in assets
'ValueError': ValueError,
'UserError': UserError,
'AccessDenied': AccessDenied,
'AccessError': AccessError,
'MissingError': MissingError,
'ValidationError': ValidationError,
'warning': lambda *args: _logger.warning(*args),
**_BUILTINS,
}
# helpers for compilation
def _append_text(self, text, compile_context):
""" Add an item (converts to a string) to the list.
This will be concatenated and added during a call to the
`_flush_text` method. This makes it possible to return only one
yield containing all the parts."""
compile_context['_text_concat'].append(self._compile_to_str(text))
def _rstrip_text(self, compile_context):
""" The text to flush is right stripped, and the stripped content are
returned.
"""
text_concat = compile_context['_text_concat']
if not text_concat:
return ''
result = RSTRIP_REGEXP.search(text_concat[-1])
strip = result.group(0) if result else ''
text_concat[-1] = RSTRIP_REGEXP.sub('', text_concat[-1])
return strip
def _flush_text(self, compile_context, level, rstrip=False):
"""Concatenate all the textual chunks added by the `_append_text`
method into a single yield.
If no text to flush, return an empty list
If rstrip the text is right stripped.
@returns list(str)
"""
text_concat = compile_context['_text_concat']
if not text_concat:
return []
if rstrip:
self._rstrip_text(compile_context)
text = ''.join(text_concat)
text_concat.clear()
return [f"{' ' * level}yield {text!r}"]
def _is_static_node(self, el, compile_context):
""" Test whether the given element is purely static, i.e. (there
are no t-* attributes), does not require dynamic rendering for its
attributes.
"""
return el.tag != 't' and 'groups' not in el.attrib and not any(
att.startswith('t-') and att not in ('t-tag-open', 't-inner-content')
for att in el.attrib
)
# compile python expression and format string
def _compile_format(self, expr):
""" Parses the provided format string and compiles it to a single
expression python, uses string with format method.
Use format is faster to concat string and values.
"""
# <t t-setf-name="Hello #{world} %s !"/>
# =>
# values['name'] = 'Hello %s %%s !' % (values['world'],)
values = [
f'self._compile_to_str({self._compile_expr(m.group(1) or m.group(2))})'
for m in FORMAT_REGEX.finditer(expr)
]
code = repr(FORMAT_REGEX.sub('%s', expr.replace('%', '%%')))
if values:
code += f' % ({", ".join(values)},)'
return code
def _compile_expr_tokens(self, tokens, allowed_keys, argument_names=None, raise_on_missing=False):
""" Transform the list of token coming into a python instruction in
textual form by adding the namepaces for the dynamic values.
Example: `5 + a + b.c` to be `5 + values.get('a') + values['b'].c`
Unknown values are considered to be None, but using `values['b']`
gives a clear error message in cases where there is an attribute for
example (have a `KeyError: 'b'`, instead of `AttributeError: 'NoneType'
object has no attribute 'c'`).
@returns str
"""
# Finds and extracts the current "scope"'s "allowed values": values
# which should not be accessed through the environment's namespace:
# * the local variables of a lambda should be accessed directly e.g.
# lambda a: a + b should be compiled to lambda a: a + values['b'],
# since a is local to the lambda it has to be accessed directly
# but b needs to be accessed through the rendering environment
# * similarly for a comprehensions [a + b for a in c] should be
# compiledto [a + values.get('b') for a in values.get('c')]
# to avoid the risk of confusion between nested lambdas / comprehensions,
# this is currently performed independently at each level of brackets
# nesting (hence the function being recursive).
open_bracket_index = -1
bracket_depth = 0