ir_qweb.py 114 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. """
  3. ================
  4. IrQWeb / ir.qweb
  5. ================
  6. Preamble
  7. ========
  8. Technical documentation of the python operation of the rendering QWeb engine.
  9. Templating
  10. ==========
  11. QWeb is the primary templating engine used by Odoo. It is an XML templating
  12. engine and used mostly to generate XML, HTML fragments and pages.
  13. Template directives are specified as XML attributes prefixed with ``t-``,
  14. for instance ``t-if`` for :ref:`reference/qweb/conditionals`, with elements
  15. and other attributes being rendered directly.
  16. To avoid element rendering, a placeholder element ``<t>`` is also available,
  17. which executes its directive but doesn't generate any output in and of
  18. itself.
  19. To create new XML template, please see :doc:`QWeb Templates documentation
  20. <https://www.odoo.com/documentation/16.0/developer/reference/frontend/qweb.html>`
  21. Rendering process
  22. =================
  23. In **input** you have an XML template giving the corresponding input etree.
  24. Each etree input nodes are used to generate a python function. This fonction is
  25. called and will give the XML **output**.
  26. The ``_compile`` method is responsible to generate the function from the
  27. etree, that function is a python generator that yield one output line at a
  28. time. This generator is consumed by ``_render``. The generated function is orm
  29. cached.
  30. For performance, the **compile time** (when input, XML template or template
  31. id, is compiled into a function) is less important than the **rendering time**
  32. (when the function is called with the different values). The generation of the
  33. function is only done once (for a set of options, language, branding ...)
  34. because it is cached orm
  35. The output is in ``MarkupSafe`` format. ``MarkupSafe`` escapes characters so
  36. text is safe to use in HTML and XML. Characters that have special meanings
  37. are replaced so that they display as the actual characters. This mitigates
  38. injection attacks, meaning untrusted user input can safely be displayed on a
  39. page.
  40. At **compile time**, each dynamic attribute ``t-*`` will be compiled into
  41. specific python code. (For example ``<t t-out="5 + 5"/>`` will insert the
  42. template "10" inside the output)
  43. At **compile time**, each directive removes the dynamic attribute it uses from
  44. the input node attributes. At the end of the compilation each input node, no
  45. dynamic attributes must remain.
  46. How the code works
  47. ==================
  48. In the graphic below you can see theresume of the call of the methods performed
  49. in the IrQweb class.
  50. .. code-block:: rst
  51. Odoo
  52. ┗━► _render (returns MarkupSafe)
  53. ┗━► _compile (returns function) ◄━━━━━━━━━━┓
  54. ┗━► _compile_node (returns code string array) ◄━━━━━━━━┓ ┃
  55. ┃ (skip the current node if found t-qweb-skip) ┃ ┃
  56. ┃ (add technical directives: t-tag-open, t-tag-close, t-inner-content) ┃ ┃
  57. ┃ ┃ ┃
  58. ┣━► _directives_eval_order (defined directive order) ┃ ┃
  59. ┣━► _compile_directives (loop) Consume all remaining directives ◄━━━┓ ┃ ┃
  60. ┃ ┃ (e.g.: to change the indentation) ┃ ┃ ┃
  61. ┃ ┣━► _compile_directive ┃ ┃ ┃
  62. ┃ ┃ ┗━► t-nocache ━━► _compile_directive_nocache ━┫ ┃ ┃
  63. ┃ ┃ ┗━► t-cache ━━► _compile_directive_cache ━┫ ┃ ┃
  64. ┃ ┃ ┗━► t-groups ━━► _compile_directive_groups ━┫ ┃ ┃
  65. ┃ ┃ ┗━► t-foreach ━━► _compile_directive_foreach ━┫ ┃ ┃
  66. ┃ ┃ ┗━► t-if ━━► _compile_directive_if ━┛ ┃ ┃
  67. ┃ ┃ ┗━► t-inner-content ━━► _compile_directive_inner_content ◄━━━━━┓ ━┛ ┃
  68. ┃ ┃ ┗━► t-options ━━► _compile_directive_options ┃ ┃
  69. ┃ ┃ ┗━► t-set ━━► _compile_directive_set ◄━━┓ ┃ ┃
  70. ┃ ┃ ┗━► t-call ━━► _compile_directive_call ━┛ ━┫ ━━━┛
  71. ┃ ┃ ┗━► t-att ━━► _compile_directive_att ┃
  72. ┃ ┃ ┗━► t-tag-open ━━► _compile_directive_open ◄━━┓ ┃
  73. ┃ ┃ ┗━► t-tag-close ━━► _compile_directive_close ◄━━┫ ┃
  74. ┃ ┃ ┗━► t-out ━━► _compile_directive_out ━┛ ━┫ ◄━━┓
  75. ┃ ┃ ┗━► t-field ━━► _compile_directive_field ┃ ━┫
  76. ┃ ┃ ┗━► t-esc ━━► _compile_directive_esc ┃ ━┛
  77. ┃ ┃ ┗━► t-* ━━► ... ┃
  78. ┃ ┃ ┃
  79. ┗━━┻━► _compile_static_node ━┛
  80. The QWeb ``_render`` uses the function generated by the ``_compile`` method.
  81. Each XML node will go through the ``_compile_node`` method. If the
  82. node does not have dynamic directives or attributes (``_is_static_node``).
  83. A ``static`` is a node without ``t-*`` attributes, does not require dynamic
  84. rendering for its attributes.
  85. If it's a ``static`` node, the ``_compile_static_node`` method is called,
  86. otherwise it is the ``_compile_directives`` method after having prepared the
  87. order for calling the directives using the ``_directives_eval_order`` method.
  88. In the defined order, for each directive the method ``_compile_directive`` is
  89. called which itself dispatches to the methods corresponding to the directives
  90. ``_compile_directive_[name of the directive]`` (for example: ``t-if`` =>
  91. ``_compile_directive_if``). After all ordered directives, the directives
  92. attributes still present on the element are compiled.
  93. The ``_post_processing_att`` method is used for the generation of rendering
  94. attributes. If the attributes come from static XML template nodes then the
  95. method is called only once when generating the render function. Otherwise the
  96. method is called during each rendering.
  97. Each expression is compiled by the method ``_compile_expr`` into a python
  98. expression whose values are namespaced.
  99. Directives
  100. ----------
  101. ``t-debug``
  102. ~~~~~~~~~~~
  103. **Values**: ``pdb``, ``ipdb``, ``pudb``, ``wdb``
  104. Activate the choosed debugger.
  105. When dev mode is enabled this allows python developers to have access to the
  106. state of variables being rendered. The code generated by the QWeb engine is
  107. not accessible, only the variables (values, self) can be analyzed or the
  108. methods that called the QWeb rendering.
  109. ``t-if``
  110. ~~~~~~~~
  111. **Values**: python expression
  112. Add an python ``if`` condition to the code string array, and call
  113. ``_compile_directives`` to level and add the code string array corresponding
  114. to the other directives and content.
  115. The structure of the dom is checked to possibly find a ``t-else`` or
  116. ``t-elif``. If these directives exist then the compilation is performed and
  117. the nodes are marked not to be rendered twice.
  118. At **rendering time** the other directives code and content will used only if
  119. the expression is evaluated as truely.
  120. The ``t-else``, ``t-elif`` and ``t-if`` are not compiled at the same time like
  121. defined in ``_directives_eval_order`` method.
  122. ```
  123. <t t-set="check" t-value="1"/>
  124. <section t-if="False">10</section>
  125. <span t-elif="check == 1" t-foreach="range(3)" t-as="check" t-esc="check"/>
  126. <section t-if="False">10</section>
  127. <div t-else="" t-if="check == 1" t-foreach="range(3)" t-as="check" t-esc="check"/>
  128. Result:
  129. <span>0</span>
  130. <span>1</span>
  131. <span>2</span>
  132. <div>1</div>
  133. ```
  134. ``t-else``
  135. ~~~~~~~~~~
  136. **Values**: nothing
  137. Only validate the **input**, the compilation if inside the ``t-if`` directive.
  138. ``t-elif``
  139. ~~~~~~~~~~
  140. **Values**: python expression
  141. Only validate the **input**, the compilation if inside the ``t-if`` directive.
  142. ``t-groups`` (``groups`` is an alias)
  143. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  144. **Values**: name of the allowed odoo user group, or preceded by ``!`` for
  145. prohibited groups
  146. The generated code uses ``user_has_groups`` Odoo method.
  147. ``t-foreach``
  148. ~~~~~~~~~~~~~
  149. **Values**: an expression returning the collection to iterate on
  150. This directive is used with ``t-as`` directive to defined the key name. The
  151. directive will be converted into a ``for`` loop. In this loop, different values
  152. are added to the dict (``values`` in the generated method) in addition to the
  153. key defined by ``t-name``, these are (``*_value``, ``*_index``, ``*_size``,
  154. ``*_first``, ``*_last``).
  155. ``t-as``
  156. ~~~~~~~~
  157. **Values**: key name
  158. The compilation method only validates if ``t-as`` and ``t-foreach`` are on the
  159. same node.
  160. ``t-options`` and ``t-options-*``
  161. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  162. **Values**: python expression
  163. It's use on the same node of another directive, it's used to configure the
  164. other directive. Used on the same ``input node`` of the directives ``t-call``,
  165. ``t-field`` or ``t-out``.
  166. Create a ``values['__qweb_options__']`` dict from the optional ``t-options``
  167. expression and add each key-value ``t-options-key="expression value"`` to this
  168. dict. (for example: ``t-options="{'widget': 'float'}"`` is equal to
  169. ``t-options-widget="'float'"``)
  170. ``t-att``, ``t-att-*`` and ``t-attf-*``
  171. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  172. **Values**: python expression (or format string expression for ``t-attf-``)
  173. Compile the attributes to create ``values['__qweb_attrs__']`` dictionary code
  174. in the compiled function. Use the ``t-att`` expression and add each key-value
  175. ``t-att-key="expression value"`` to this dict. (for example:
  176. ``t-att="{'class': f'float_{1}'}"`` is equal to ``t-att-class="f'float_{1}'"``
  177. and is equal to ``t-attf-class="float_{{1}}")
  178. The attributes come from new namespaces, static elements (not preceded
  179. by ``t-``) and dynamic attributes ``t-att``, attributes prefixed by ``t-att-``
  180. (python expression) or ``t-attf`` (format string expression).
  181. ``t-call``
  182. ~~~~~~~~~~
  183. **Values**: format string expression for template name
  184. Serves the called template in place of the current ``t-call`` node.
  185. Here are the different steps performed by the generated python code:
  186. #. copy the ``values`` dictionary;
  187. #. render the content (``_compile_directive_inner_content``) of the tag in a
  188. separate method called with the previous copied values. This values can be
  189. updated via t-set. The visible content of the rendering of the sub-content
  190. is added as a magical value ``0`` (can be rendered with ``t-out="0"``);
  191. #. copy the ``compile_context`` dictionary;
  192. #. compile the directive ``t-options`` and update the ``compile_context``
  193. are, in added to the calling template and the ``nsmap`` values;
  194. #. get the compiled function from the ``_compile`` method;
  195. #. use the compiled function to serves the called template.
  196. ``t-lang``
  197. ~~~~~~~~~~
  198. **Values**: python expression
  199. Used to serve the called template (``t-call``) in another language. Used
  200. together with ``t-call``.
  201. This directive will be evaluate like ``t-options-lang``. Allows you to change
  202. the language in which the called template is rendered. It's in the ``t-call``
  203. directive that the language of the context of the ``ir.qweb`` recordset on
  204. which the ``_compile`` function is called is updated.
  205. ``t-call-assets``
  206. ~~~~~~~~~~~~~~~~~
  207. **Values**: format string for template name
  208. The generated code call the ``_get_asset_nodes`` method to get the list of
  209. (tagName, attrs and content). From each tuple a tag is created into the
  210. rendering.
  211. ``t-out``
  212. ~~~~~~~~~
  213. **Values**: python expression
  214. Output the given value or if falsy, display the content as default value.
  215. (for example: ``<t t-out="given_value">Default content</t>``)
  216. The generated code add the value into the ``MarkupSafe`` rendering.
  217. If a widget is defined (``t-options-widget``), the generated code call the
  218. ``_get_widget`` method to have the formatted field value and attributes. It's
  219. the ``ir.qweb.field.*`` models that format the value.
  220. ``t-field``
  221. ~~~~~~~~~~~
  222. **Values**: String representing the path to the field. (for example:
  223. ``t-field="record.name"``)
  224. Output the field value or if falsy, display the content as default value.
  225. (for example: ``<span t-field="record.name">Default content</span>``)
  226. Use ``t-out`` compile method but the generated code call ``_get_field``
  227. instead of ``_get_widget``. It's the ``ir.qweb.field.*`` models that format
  228. the value. The rendering model is chosen according to the type of field. The
  229. rendering model can be modified via the ``t-options-widget``.
  230. ``t-esc``
  231. ~~~~~~~~~
  232. Deprecated, please use ``t-out``
  233. ``t-raw``
  234. ~~~~~~~~~
  235. Deprecated, please use ``t-out``
  236. ``t-set``
  237. ~~~~~~~~~
  238. **Values**: key name
  239. The generated code update the key ``values`` dictionary equal to the value
  240. defined by ``t-value`` expression, ``t-valuef`` format string expression or
  241. to the ``MarkupSafe`` rendering come from the content of the node.
  242. ``t-value``
  243. ~~~~~~~~~~~
  244. **Values**: python expression
  245. The compilation method only validates if ``t-value`` and ``t-set`` are on the
  246. same node.
  247. ``t-valuef``
  248. ~~~~~~~~~~~~
  249. **Values**: format string expression
  250. The compilation method only validates if ``t-valuef`` and ``t-set`` are on the
  251. same node.
  252. Technical directives
  253. --------------------
  254. Directive added automatically by IrQweb in order to go through the compilation
  255. methods.
  256. ``t-tag-open``
  257. ~~~~~~~~~~~~~~
  258. Used to generate the opening HTML/XML tags.
  259. ``t-tag-close``
  260. ~~~~~~~~~~~~~~
  261. Used to generate the closing HTML/XML tags.
  262. ``t-inner-content``
  263. ~~~~~~~~~~~~~~~~~~~
  264. Used to add the content of the node (text, tail and children nodes).
  265. If namespaces are declared on the current element then a copy of the options
  266. is made.
  267. ``t-consumed-options``
  268. ~~~~~~~~~~~~~~~~~~~~~~
  269. Raise an exception if the ``t-options`` is not consumed.
  270. ``t-qweb-skip``
  271. ~~~~~~~~~~~~~~~~~~~~~~
  272. Ignore rendering and directives for the curent **input** node.
  273. ``t-else-valid``
  274. ~~~~~~~~~~~~~~~~~~~~~~
  275. Mark a node with ``t-else`` or ``t-elif`` having a valid **input** dom
  276. structure.
  277. """
  278. import fnmatch
  279. import io
  280. import logging
  281. import math
  282. import re
  283. import textwrap
  284. import time
  285. import token
  286. import tokenize
  287. import traceback
  288. import werkzeug
  289. from markupsafe import Markup, escape
  290. from collections.abc import Sized, Mapping
  291. from itertools import count, chain
  292. from lxml import etree
  293. from dateutil.relativedelta import relativedelta
  294. from psycopg2.extensions import TransactionRollbackError
  295. from odoo import api, models, tools
  296. from odoo.tools import config, safe_eval, pycompat, SUPPORTED_DEBUGGER
  297. from odoo.tools.safe_eval import assert_valid_codeobj, _BUILTINS, to_opcodes, _EXPR_OPCODES, _BLACKLIST
  298. from odoo.tools.json import scriptsafe
  299. from odoo.tools.misc import str2bool
  300. from odoo.tools.image import image_data_uri
  301. from odoo.http import request
  302. from odoo.modules.module import get_resource_path, get_module_path
  303. from odoo.tools.profiler import QwebTracker
  304. from odoo.exceptions import UserError, AccessDenied, AccessError, MissingError, ValidationError
  305. from odoo.addons.base.models.assetsbundle import AssetsBundle
  306. from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS, TEMPLATE_EXTENSIONS
  307. _logger = logging.getLogger(__name__)
  308. # QWeb token usefull for generate expression used in `_compile_expr_tokens` method
  309. token.QWEB = token.NT_OFFSET - 1
  310. token.tok_name[token.QWEB] = 'QWEB'
  311. # security safe eval opcodes for generated expression validation, used in `_compile_expr`
  312. _SAFE_QWEB_OPCODES = _EXPR_OPCODES.union(to_opcodes([
  313. 'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX',
  314. 'CALL_METHOD', 'LOAD_METHOD',
  315. 'GET_ITER', 'FOR_ITER', 'YIELD_VALUE',
  316. 'JUMP_FORWARD', 'JUMP_ABSOLUTE', 'JUMP_BACKWARD',
  317. 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE', 'POP_JUMP_IF_TRUE',
  318. 'LOAD_NAME', 'LOAD_ATTR',
  319. 'LOAD_FAST', 'STORE_FAST', 'UNPACK_SEQUENCE',
  320. 'STORE_SUBSCR',
  321. 'LOAD_GLOBAL',
  322. # Following opcodes were added in 3.11 https://docs.python.org/3/whatsnew/3.11.html#new-opcodes
  323. 'RESUME',
  324. 'CALL',
  325. 'PRECALL',
  326. 'POP_JUMP_FORWARD_IF_FALSE',
  327. 'PUSH_NULL',
  328. 'POP_JUMP_FORWARD_IF_TRUE', 'KW_NAMES',
  329. 'FORMAT_VALUE', 'BUILD_STRING',
  330. 'RETURN_GENERATOR',
  331. 'POP_JUMP_BACKWARD_IF_FALSE',
  332. 'SWAP',
  333. ])) - _BLACKLIST
  334. # eval to compile generated string python code into binary code, used in `_compile`
  335. unsafe_eval = eval
  336. VOID_ELEMENTS = frozenset([
  337. 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
  338. 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
  339. # Terms allowed in addition to AVAILABLE_OBJECTS when compiling python expressions
  340. ALLOWED_KEYWORD = frozenset(['False', 'None', 'True', 'and', 'as', 'elif', 'else', 'for', 'if', 'in', 'is', 'not', 'or'] + list(_BUILTINS))
  341. # regexpr for string formatting and extract ( ruby-style )|( jinja-style ) used in `_compile_format`
  342. FORMAT_REGEX = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
  343. RSTRIP_REGEXP = re.compile(r'\n[ \t]*$')
  344. LSTRIP_REGEXP = re.compile(r'^[ \t]*\n')
  345. FIRST_RSTRIP_REGEXP = re.compile(r'^(\n[ \t]*)+(\n[ \t])')
  346. VARNAME_REGEXP = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
  347. TO_VARNAME_REGEXP = re.compile(r'[^A-Za-z0-9_]+')
  348. # Attribute name used outside the context of the QWeb.
  349. SPECIAL_DIRECTIVES = {'t-translation', 't-ignore', 't-title'}
  350. # Name of the variable to insert the content in t-call in the template.
  351. # The slot will be replaced by the `t-call` tag content of the caller.
  352. T_CALL_SLOT = '0'
  353. def indent_code(code, level):
  354. """Indent the code to respect the python syntax."""
  355. return textwrap.indent(textwrap.dedent(code).strip(), ' ' * 4 * level)
  356. def keep_query(*keep_params, **additional_params):
  357. """
  358. Generate a query string keeping the current request querystring's parameters specified
  359. in ``keep_params`` and also adds the parameters specified in ``additional_params``.
  360. Multiple values query string params will be merged into a single one with comma seperated
  361. values.
  362. The ``keep_params`` arguments can use wildcards too, eg:
  363. keep_query('search', 'shop_*', page=4)
  364. """
  365. if not keep_params and not additional_params:
  366. keep_params = ('*',)
  367. params = additional_params.copy()
  368. qs_keys = list(request.httprequest.args) if request else []
  369. for keep_param in keep_params:
  370. for param in fnmatch.filter(qs_keys, keep_param):
  371. if param not in additional_params and param in qs_keys:
  372. params[param] = request.httprequest.args.getlist(param)
  373. return werkzeug.urls.url_encode(params)
  374. ####################################
  375. ### QWebException ###
  376. ####################################
  377. class QWebException(Exception):
  378. """ Management of errors that raised when rendering a QWeb template.
  379. """
  380. def __init__(self, message, qweb, template=None, ref=None, path_xml=None, code=None):
  381. self.stack = traceback.format_exc()
  382. self.name = template
  383. self.ref = ref
  384. self.path, self.html = path_xml or (None, None)
  385. self.code = None
  386. if code:
  387. self.code = '\n'.join(code.split('\n')[:-1]) if qweb.env.context.get('dev_mode') else None
  388. line_nb = 0
  389. for error_line in reversed(self.stack.split('\n')):
  390. if f'File "<{self.ref}>"' in error_line:
  391. line_function = error_line.split(', line ')[1]
  392. line_nb = int(line_function.split(',')[0])
  393. break
  394. for code_line in reversed(code.split('\n')[:line_nb]):
  395. match = re.match(r'\s*# element: (.*) , (.*)', code_line)
  396. if match:
  397. self.path = match[1][1:-1]
  398. self.html = match[2][1:-1]
  399. break
  400. self.title = message
  401. super().__init__(message)
  402. def __str__(self):
  403. parts = [self.title]
  404. if self.__cause__ and str(self.__cause__) != '':
  405. parts.append(f"{self.__cause__.__class__.__name__}: {self.__cause__}")
  406. elif self.__context__ and str(self.__context__) != '':
  407. parts.append(f"{self.__context__.__class__.__name__}: {self.__context__}")
  408. if self.name is not None:
  409. parts.append(f"Template: {self.name}")
  410. if self.path is not None:
  411. parts.append(f"Path: {self.path}")
  412. if self.html is not None:
  413. parts.append(f"Node: {self.html}")
  414. if self.code is not None:
  415. parts.append(f"Compiled code:\n{self.code}")
  416. return "\n".join(parts)
  417. def __repr__(self):
  418. return f"QWebException({self.title!r})"
  419. ####################################
  420. ### QWeb ###
  421. ####################################
  422. class IrQWeb(models.AbstractModel):
  423. """ Base QWeb rendering engine
  424. * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
  425. create new models called :samp:`ir.qweb.field.{widget}`
  426. Beware that if you need extensions or alterations which could be
  427. incompatible with other subsystems, you should create a local object
  428. inheriting from ``ir.qweb`` and customize that.
  429. """
  430. _name = 'ir.qweb'
  431. _description = 'Qweb'
  432. @QwebTracker.wrap_render
  433. @api.model
  434. def _render(self, template, values=None, **options):
  435. """ render(template, values, **options)
  436. Render the template specified by the given name.
  437. :param template: etree, xml_id, template name (see _get_template)
  438. * Call the method ``load`` is not an etree.
  439. :param dict values: template values to be used for rendering
  440. :param options: used to compile the template
  441. Options will be add into the IrQweb.env.context for the rendering.
  442. * ``lang`` (str) used language to render the template
  443. * ``inherit_branding`` (bool) add the tag node branding
  444. * ``inherit_branding_auto`` (bool) add the branding on fields
  445. * ``minimal_qcontext``(bool) To use the minimum context and options
  446. from ``_prepare_environment``
  447. :returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
  448. instead of `str`)
  449. :rtype: MarkupSafe
  450. """
  451. values = values.copy() if values else {}
  452. if T_CALL_SLOT in values:
  453. raise ValueError(f'values[{T_CALL_SLOT}] should be unset when call the _render method and only set into the template.')
  454. irQweb = self.with_context(**options)._prepare_environment(values)
  455. safe_eval.check_values(values)
  456. template_functions, def_name = irQweb._compile(template)
  457. render_template = template_functions[def_name]
  458. rendering = render_template(irQweb, values)
  459. result = ''.join(rendering)
  460. return Markup(result)
  461. # assume cache will be invalidated by third party on write to ir.ui.view
  462. def _get_template_cache_keys(self):
  463. """ Return the list of context keys to use for caching ``_compile``. """
  464. return ['lang', 'inherit_branding', 'edit_translations', 'profile']
  465. @tools.conditional(
  466. 'xml' not in tools.config['dev_mode'],
  467. tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
  468. )
  469. def _get_view_id(self, template):
  470. try:
  471. return self.env['ir.ui.view'].sudo().with_context(load_all_views=True)._get_view_id(template)
  472. except Exception:
  473. return None
  474. @QwebTracker.wrap_compile
  475. def _compile(self, template):
  476. if isinstance(template, etree._Element):
  477. self = self.with_context(is_t_cache_disabled=True)
  478. ref = None
  479. else:
  480. ref = self._get_view_id(template)
  481. # define the base key cache for code in cache and t-cache feature
  482. base_key_cache = None
  483. if ref:
  484. base_key_cache = self._get_cache_key(tuple([ref] + [self.env.context.get(k) for k in self._get_template_cache_keys()]))
  485. self = self.with_context(__qweb_base_key_cache=base_key_cache)
  486. # generate the template functions and the root function name
  487. def generate_functions():
  488. code, options, def_name = self._generate_code(template)
  489. profile_options = {
  490. 'ref': options.get('ref') and int(options['ref']) or None,
  491. 'ref_xml': options.get('ref_xml') and str(options['ref_xml']) or None,
  492. } if self.env.context.get('profile') else None
  493. code = '\n'.join([
  494. "def generate_functions():",
  495. " template_functions = {}",
  496. indent_code(code, 1),
  497. f" template_functions['options'] = {profile_options!r}",
  498. " return template_functions",
  499. ])
  500. try:
  501. compiled = compile(code, f"<{ref}>", 'exec')
  502. globals_dict = self.__prepare_globals()
  503. globals_dict['__builtins__'] = globals_dict # So that unknown/unsafe builtins are never added.
  504. unsafe_eval(compiled, globals_dict)
  505. return globals_dict['generate_functions'](), def_name
  506. except QWebException:
  507. raise
  508. except Exception as e:
  509. raise QWebException("Error when compiling xml template",
  510. self, template, code=code, ref=ref) from e
  511. return self._load_values(base_key_cache, generate_functions)
  512. def _generate_code(self, template):
  513. """ Compile the given template into a rendering function (generator)::
  514. render_template(qweb, values)
  515. This method can be called only by the IrQweb `_render` method or by
  516. the compiled code of t-call from an other template.
  517. An `options` dictionary is created and attached to the function. It
  518. contains rendering options that are part of the cache key in
  519. addition to template references.
  520. where ``qweb`` is a QWeb instance and ``values`` are the values to
  521. render.
  522. :returns: tuple containing code, options and main method name
  523. """
  524. # The `compile_context`` dictionary includes the elements used for the
  525. # cache key to which are added the template references as well as
  526. # technical information useful for generating the function. This
  527. # dictionary is only used when compiling the template.
  528. compile_context = self.env.context.copy()
  529. try:
  530. element, document, ref = self._get_template(template)
  531. except (ValueError, UserError) as e:
  532. # return the error function if the template is not found or fail
  533. message = str(e)
  534. code = indent_code(f"""
  535. def not_found_template(self, values):
  536. if self.env.context.get('raise_if_not_found', True):
  537. raise {e.__class__.__name__}({message!r})
  538. warning('Cannot load template %s: %s', {template!r}, {message!r})
  539. return ''
  540. template_functions = {{'not_found_template': not_found_template}}
  541. """, 0)
  542. return (code, {}, 'not_found_template')
  543. compile_context.pop('raise_if_not_found', None)
  544. # reference to get xml and etree (usually the template ID)
  545. compile_context['ref'] = ref
  546. # reference name or key to get xml and etree (usually the template XML ID)
  547. compile_context['ref_name'] = element.attrib.pop('t-name', template if isinstance(template, str) and '<' not in template else None)
  548. # str xml of the reference template used for compilation. Useful for debugging, dev mode and profiling.
  549. compile_context['ref_xml'] = document
  550. # Identifier used to call `_compile`
  551. compile_context['template'] = template
  552. # Root of the etree which will be processed during compilation.
  553. compile_context['root'] = element.getroottree()
  554. # Reference to the last node being compiled. It is mainly used for debugging and displaying error messages.
  555. compile_context['_qweb_error_path_xml'] = None
  556. if not compile_context.get('nsmap'):
  557. compile_context['nsmap'] = {}
  558. # The options dictionary includes cache key elements and template
  559. # references. It will be attached to the generated function. This
  560. # dictionary is only there for logs, performance or test information.
  561. # The values of these `options` cannot be changed and must always be
  562. # identical in `context` and `self.env.context`.
  563. options = {k: compile_context.get(k) for k in self._get_template_cache_keys() + ['ref', 'ref_name', 'ref_xml']}
  564. # generate code
  565. def_name = TO_VARNAME_REGEXP.sub(r'_', f'template_{ref}')
  566. name_gen = count()
  567. compile_context['make_name'] = lambda prefix: f"{def_name}_{prefix}_{next(name_gen)}"
  568. try:
  569. if element.text:
  570. element.text = FIRST_RSTRIP_REGEXP.sub(r'\2', element.text)
  571. compile_context['template_functions'] = {}
  572. compile_context['_text_concat'] = []
  573. self._append_text("", compile_context) # To ensure the template function is a generator and doesn't become a regular function
  574. compile_context['template_functions'][f'{def_name}_content'] = (
  575. [f"def {def_name}_content(self, values):"]
  576. + self._compile_node(element, compile_context, 2)
  577. + self._flush_text(compile_context, 2, rstrip=True))
  578. compile_context['template_functions'][def_name] = [indent_code(f"""
  579. def {def_name}(self, values):
  580. try:
  581. if '__qweb_loaded_values' not in values:
  582. values['__qweb_loaded_values'] = {{}}
  583. values['__qweb_root_values'] = values.copy()
  584. values['xmlid'] = {options['ref_name']!r}
  585. values['viewid'] = {options['ref']!r}
  586. values['__qweb_loaded_values'].update(template_functions)
  587. yield from {def_name}_content(self, values)
  588. except QWebException:
  589. raise
  590. except Exception as e:
  591. if isinstance(e, TransactionRollbackError):
  592. raise
  593. raise QWebException("Error while render the template",
  594. self, template, ref={compile_context['ref']!r}, code=code) from e
  595. """, 0)]
  596. except QWebException:
  597. raise
  598. except Exception as e:
  599. raise QWebException("Error when compiling xml template",
  600. self, template, ref=compile_context['ref'], path_xml=compile_context['_qweb_error_path_xml']) from e
  601. code_lines = ['code = None']
  602. code_lines.append(f'template = {(document if isinstance(template, etree._Element) else template)!r}')
  603. code_lines.append('template_functions = {}')
  604. for lines in compile_context['template_functions'].values():
  605. code_lines.extend(lines)
  606. for name in compile_context['template_functions']:
  607. code_lines.append(f'template_functions[{name!r}] = {name}')
  608. code = '\n'.join(code_lines)
  609. code += f'\n\ncode = {code!r}'
  610. return (code, options, def_name)
  611. # read and load input template
  612. def _get_template(self, template):
  613. """ Retrieve the given template, and return it as a tuple ``(etree,
  614. xml, ref)``, where ``element`` is an etree, ``document`` is the
  615. string document that contains ``element``, and ``ref`` if the uniq
  616. reference of the template (id, t-name or template).
  617. :param template: template identifier or etree
  618. """
  619. assert template not in (False, None, ""), "template is required"
  620. # template is an xml etree already
  621. if isinstance(template, etree._Element):
  622. element = template
  623. document = etree.tostring(template, encoding='unicode')
  624. ref = None
  625. # template is xml as string
  626. elif isinstance(template, str) and '<' in template:
  627. raise ValueError('Inline templates must be passed as `etree` documents')
  628. # template is (id or ref) to a database stored template
  629. else:
  630. try:
  631. ref_alias = int(template) # e.g. <t t-call="33"/>
  632. except ValueError:
  633. ref_alias = template # e.g. web.layout
  634. doc_or_elem, ref = self._load(ref_alias) or (None, None)
  635. if doc_or_elem is None:
  636. raise ValueError(f"Can not load template: {ref_alias!r}")
  637. if isinstance(doc_or_elem, etree._Element):
  638. element = doc_or_elem
  639. document = etree.tostring(doc_or_elem, encoding='unicode')
  640. elif isinstance(doc_or_elem, str):
  641. element = etree.fromstring(doc_or_elem)
  642. document = doc_or_elem
  643. else:
  644. raise TypeError(f"Loaded template {ref!r} should be a string.")
  645. # return etree, document and ref, or try to find the ref
  646. if ref:
  647. return (element, document, ref)
  648. # <templates>
  649. # <template t-name=... /> <!-- return ONLY this element -->
  650. # <template t-name=... />
  651. # </templates>
  652. for node in element.iter():
  653. ref = node.get('t-name')
  654. if ref:
  655. return (node, document, ref)
  656. # use the document itself as ref when no t-name was found
  657. return (element, document, document)
  658. def _load(self, ref):
  659. """
  660. Load the template referenced by ``ref``.
  661. :returns: The loaded template (as string or etree) and its
  662. identifier
  663. :rtype: Tuple[Union[etree, str], Optional[str, int]]
  664. """
  665. IrUIView = self.env['ir.ui.view'].sudo()
  666. view = IrUIView._get(ref)
  667. template = IrUIView._read_template(view.id)
  668. etree_view = etree.fromstring(template)
  669. xmlid = view.key or ref
  670. if isinstance(ref, int):
  671. domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', view.id)]
  672. model_data = self.env['ir.model.data'].sudo().search_read(domain, ['module', 'name'], limit=1)
  673. if model_data:
  674. xmlid = f"{model_data[0]['module']}.{model_data[0]['name']}"
  675. # QWeb's ``_read_template`` will check if one of the first children of
  676. # what we send to it has a "t-name" attribute having ``ref`` as value
  677. # to consider it has found it. As it'll never be the case when working
  678. # with view ids or children view or children primary views, force it here.
  679. if view.inherit_id is not None:
  680. for node in etree_view:
  681. if node.get('t-name') == str(ref) or node.get('t-name') == str(view.key):
  682. node.attrib.pop('name', None)
  683. node.attrib.pop('id', None)
  684. etree_view = node
  685. break
  686. etree_view.set('t-name', str(xmlid))
  687. return (etree_view, view.id)
  688. # values for running time
  689. def _prepare_environment(self, values):
  690. """ Prepare the values and context that will sent to the
  691. compiled and evaluated function.
  692. :param values: template values to be used for rendering
  693. :returns self (with new context)
  694. """
  695. debug = request and request.session.debug or ''
  696. values.update(
  697. true=True,
  698. false=False,
  699. )
  700. if not self.env.context.get('minimal_qcontext'):
  701. values.setdefault('debug', debug)
  702. values.setdefault('user_id', self.env.user.with_env(self.env))
  703. values.setdefault('res_company', self.env.company.sudo())
  704. values.update(
  705. request=request, # might be unbound if we're not in an httprequest context
  706. test_mode_enabled=bool(config['test_enable'] or config['test_file']),
  707. json=scriptsafe,
  708. quote_plus=werkzeug.urls.url_quote_plus,
  709. time=safe_eval.time,
  710. datetime=safe_eval.datetime,
  711. relativedelta=relativedelta,
  712. image_data_uri=image_data_uri,
  713. # specific 'math' functions to ease rounding in templates and lessen controller marshmalling
  714. floor=math.floor,
  715. ceil=math.ceil,
  716. env=self.env,
  717. lang=self.env.context.get('lang'),
  718. keep_query=keep_query,
  719. )
  720. context = {'dev_mode': 'qweb' in tools.config['dev_mode']}
  721. if 'xml' in tools.config['dev_mode']:
  722. context['is_t_cache_disabled'] = True
  723. elif 'disable-t-cache' in debug:
  724. context['is_t_cache_disabled'] = True
  725. return self.with_context(**context)
  726. def __prepare_globals(self):
  727. """ Prepare the global context that will sent to eval the qweb
  728. generated code.
  729. """
  730. return {
  731. 'Sized': Sized,
  732. 'Mapping': Mapping,
  733. 'Markup': Markup,
  734. 'escape': escape,
  735. 'VOID_ELEMENTS': VOID_ELEMENTS,
  736. 'QWebException': QWebException,
  737. 'Exception': Exception,
  738. 'TransactionRollbackError': TransactionRollbackError, # for SerializationFailure in assets
  739. 'ValueError': ValueError,
  740. 'UserError': UserError,
  741. 'AccessDenied': AccessDenied,
  742. 'AccessError': AccessError,
  743. 'MissingError': MissingError,
  744. 'ValidationError': ValidationError,
  745. 'warning': lambda *args: _logger.warning(*args),
  746. **_BUILTINS,
  747. }
  748. # helpers for compilation
  749. def _append_text(self, text, compile_context):
  750. """ Add an item (converts to a string) to the list.
  751. This will be concatenated and added during a call to the
  752. `_flush_text` method. This makes it possible to return only one
  753. yield containing all the parts."""
  754. compile_context['_text_concat'].append(self._compile_to_str(text))
  755. def _rstrip_text(self, compile_context):
  756. """ The text to flush is right stripped, and the stripped content are
  757. returned.
  758. """
  759. text_concat = compile_context['_text_concat']
  760. if not text_concat:
  761. return ''
  762. result = RSTRIP_REGEXP.search(text_concat[-1])
  763. strip = result.group(0) if result else ''
  764. text_concat[-1] = RSTRIP_REGEXP.sub('', text_concat[-1])
  765. return strip
  766. def _flush_text(self, compile_context, level, rstrip=False):
  767. """Concatenate all the textual chunks added by the `_append_text`
  768. method into a single yield.
  769. If no text to flush, return an empty list
  770. If rstrip the text is right stripped.
  771. @returns list(str)
  772. """
  773. text_concat = compile_context['_text_concat']
  774. if not text_concat:
  775. return []
  776. if rstrip:
  777. self._rstrip_text(compile_context)
  778. text = ''.join(text_concat)
  779. text_concat.clear()
  780. return [f"{' ' * level}yield {text!r}"]
  781. def _is_static_node(self, el, compile_context):
  782. """ Test whether the given element is purely static, i.e. (there
  783. are no t-* attributes), does not require dynamic rendering for its
  784. attributes.
  785. """
  786. return el.tag != 't' and 'groups' not in el.attrib and not any(
  787. att.startswith('t-') and att not in ('t-tag-open', 't-inner-content')
  788. for att in el.attrib
  789. )
  790. # compile python expression and format string
  791. def _compile_format(self, expr):
  792. """ Parses the provided format string and compiles it to a single
  793. expression python, uses string with format method.
  794. Use format is faster to concat string and values.
  795. """
  796. # <t t-setf-name="Hello #{world} %s !"/>
  797. # =>
  798. # values['name'] = 'Hello %s %%s !' % (values['world'],)
  799. values = [
  800. f'self._compile_to_str({self._compile_expr(m.group(1) or m.group(2))})'
  801. for m in FORMAT_REGEX.finditer(expr)
  802. ]
  803. code = repr(FORMAT_REGEX.sub('%s', expr.replace('%', '%%')))
  804. if values:
  805. code += f' % ({", ".join(values)},)'
  806. return code
  807. def _compile_expr_tokens(self, tokens, allowed_keys, argument_names=None, raise_on_missing=False):
  808. """ Transform the list of token coming into a python instruction in
  809. textual form by adding the namepaces for the dynamic values.
  810. Example: `5 + a + b.c` to be `5 + values.get('a') + values['b'].c`
  811. Unknown values are considered to be None, but using `values['b']`
  812. gives a clear error message in cases where there is an attribute for
  813. example (have a `KeyError: 'b'`, instead of `AttributeError: 'NoneType'
  814. object has no attribute 'c'`).
  815. @returns str
  816. """
  817. # Finds and extracts the current "scope"'s "allowed values": values
  818. # which should not be accessed through the environment's namespace:
  819. # * the local variables of a lambda should be accessed directly e.g.
  820. # lambda a: a + b should be compiled to lambda a: a + values['b'],
  821. # since a is local to the lambda it has to be accessed directly
  822. # but b needs to be accessed through the rendering environment
  823. # * similarly for a comprehensions [a + b for a in c] should be
  824. # compiledto [a + values.get('b') for a in values.get('c')]
  825. # to avoid the risk of confusion between nested lambdas / comprehensions,
  826. # this is currently performed independently at each level of brackets
  827. # nesting (hence the function being recursive).
  828. open_bracket_index = -1
  829. bracket_depth = 0
  830. argument_name = '_arg_%s__'
  831. argument_names = argument_names or []
  832. for index, t in enumerate(tokens):
  833. if t.exact_type in [token.LPAR, token.LSQB, token.LBRACE]:
  834. bracket_depth += 1
  835. elif t.exact_type in [token.RPAR, token.RSQB, token.RBRACE]:
  836. bracket_depth -= 1
  837. elif bracket_depth == 0 and t.exact_type == token.NAME:
  838. string = t.string
  839. if string == 'lambda': # lambda => allowed values for the current bracket depth
  840. for i in range(index + 1, len(tokens)):
  841. t = tokens[i]
  842. if t.exact_type == token.NAME:
  843. argument_names.append(t.string)
  844. elif t.exact_type == token.COMMA:
  845. pass
  846. elif t.exact_type == token.COLON:
  847. break
  848. elif t.exact_type == token.EQUAL:
  849. raise NotImplementedError('Lambda default values are not supported')
  850. else:
  851. raise NotImplementedError('This lambda code style is not implemented.')
  852. elif string == 'for': # list comprehensions => allowed values for the current bracket depth
  853. for i in range(index + 1, len(tokens)):
  854. t = tokens[i]
  855. if t.exact_type == token.NAME:
  856. if t.string == 'in':
  857. break
  858. argument_names.append(t.string)
  859. elif t.exact_type in [token.COMMA, token.LPAR, token.RPAR]:
  860. pass
  861. else:
  862. raise NotImplementedError('This loop code style is not implemented.')
  863. # Use bracket to nest structures.
  864. # Recursively processes the "sub-scopes", and replace their content with
  865. # a compiled node. During this recursive call we add to the allowed
  866. # values the values provided by the list comprehension, lambda, etc.,
  867. # previously extracted.
  868. index = 0
  869. open_bracket_index = -1
  870. bracket_depth = 0
  871. while index < len(tokens):
  872. t = tokens[index]
  873. string = t.string
  874. if t.exact_type in [token.LPAR, token.LSQB, token.LBRACE]:
  875. if bracket_depth == 0:
  876. open_bracket_index = index
  877. bracket_depth += 1
  878. elif t.exact_type in [token.RPAR, token.RSQB, token.RBRACE]:
  879. bracket_depth -= 1
  880. if bracket_depth == 0:
  881. code = self._compile_expr_tokens(
  882. tokens[open_bracket_index + 1:index],
  883. list(allowed_keys),
  884. list(argument_names),
  885. raise_on_missing,
  886. )
  887. code = tokens[open_bracket_index].string + code + t.string
  888. tokens[open_bracket_index:index + 1] = [tokenize.TokenInfo(token.QWEB, code, tokens[open_bracket_index].start, t.end, '')]
  889. index = open_bracket_index
  890. index += 1
  891. # The keys will be namespaced by values if they are not allowed. In
  892. # order to have a clear keyError message, this will be replaced by
  893. # values['key'] for certain cases (for example if an attribute is called
  894. # key.attrib, or an index key[0] ...)
  895. code = []
  896. index = 0
  897. pos = tokens and tokens[0].start # to keep level when use expr on multi line
  898. while index < len(tokens):
  899. t = tokens[index]
  900. string = t.string
  901. if t.start[0] != pos[0]:
  902. pos = (t.start[0], 0)
  903. space = t.start[1] - pos[1]
  904. if space:
  905. code.append(' ' * space)
  906. pos = t.start
  907. if t.exact_type == token.NAME:
  908. if string == 'lambda': # lambda => allowed values
  909. code.append('lambda ')
  910. index += 1
  911. while index < len(tokens):
  912. t = tokens[index]
  913. if t.exact_type == token.NAME and t.string in argument_names:
  914. code.append(argument_name % t.string)
  915. if t.exact_type in [token.COMMA, token.COLON]:
  916. code.append(t.string)
  917. if t.exact_type == token.COLON:
  918. break
  919. index += 1
  920. if t.end[0] != pos[0]:
  921. pos = (t.end[0], 0)
  922. else:
  923. pos = t.end
  924. elif string in argument_names:
  925. code.append(argument_name % t.string)
  926. elif string in allowed_keys:
  927. code.append(string)
  928. elif index + 1 < len(tokens) and tokens[index + 1].exact_type == token.EQUAL: # function kw
  929. code.append(string)
  930. elif index > 0 and tokens[index - 1] and tokens[index - 1].exact_type == token.DOT:
  931. code.append(string)
  932. elif raise_on_missing or index + 1 < len(tokens) and tokens[index + 1].exact_type in [token.DOT, token.LPAR, token.LSQB, 'qweb']:
  933. # Should have values['product'].price to raise an error when get
  934. # the 'product' value and not an 'NoneType' object has no
  935. # attribute 'price' error.
  936. code.append(f'values[{string!r}]')
  937. else:
  938. # not assignation allowed, only getter
  939. code.append(f'values.get({string!r})')
  940. elif t.type not in [tokenize.ENCODING, token.ENDMARKER, token.DEDENT]:
  941. code.append(string)
  942. if t.end[0] != pos[0]:
  943. pos = (t.end[0], 0)
  944. else:
  945. pos = t.end
  946. index += 1
  947. return ''.join(code)
  948. def _compile_expr(self, expr, raise_on_missing=False):
  949. """Transform string coming into a python instruction in textual form by
  950. adding the namepaces for the dynamic values.
  951. This method tokenize the string and call ``_compile_expr_tokens``
  952. method.
  953. :param expr: string: python expression
  954. :param [raise_on_missing]: boolean:
  955. Compile has `values['product'].price` instead of
  956. `values.get('product').price` to raise an error when get the
  957. 'product' value and not an 'NoneType' object has no attribute
  958. 'price' error.
  959. """
  960. # Parentheses are useful for compiling multi-line expressions such as
  961. # conditions existing in some templates. (see test_compile_expr tests)
  962. readable = io.BytesIO(f"({expr or ''})".encode('utf-8'))
  963. try:
  964. tokens = list(tokenize.tokenize(readable.readline))
  965. except tokenize.TokenError:
  966. raise ValueError(f"Can not compile expression: {expr}")
  967. expression = self._compile_expr_tokens(tokens, ALLOWED_KEYWORD, raise_on_missing=raise_on_missing)
  968. assert_valid_codeobj(_SAFE_QWEB_OPCODES, compile(expression, '<>', 'eval'), expr)
  969. return f"({expression})"
  970. def _compile_bool(self, attr, default=False):
  971. """Convert the statements as a boolean."""
  972. if attr:
  973. if attr is True:
  974. return True
  975. attr = attr.lower()
  976. if attr in ('false', '0'):
  977. return False
  978. elif attr in ('true', '1'):
  979. return True
  980. return bool(default)
  981. def _compile_to_str(self, expr):
  982. """ Generates a text value (an instance of text_type) from an arbitrary
  983. source.
  984. """
  985. return pycompat.to_text(expr)
  986. # order
  987. def _directives_eval_order(self):
  988. """ List all supported directives in the order in which they should be
  989. evaluated on a given element. For instance, a node bearing both
  990. ``foreach`` and ``if`` should see ``foreach`` executed before ``if`` aka
  991. .. code-block:: xml
  992. <el t-foreach="foo" t-as="bar" t-if="bar">
  993. should be equivalent to
  994. .. code-block:: xml
  995. <t t-foreach="foo" t-as="bar">
  996. <t t-if="bar">
  997. <el>
  998. then this method should return ``['foreach', 'if']``.
  999. """
  1000. return [
  1001. 'elif', # Must be the first because compiled by the previous if.
  1002. 'else', # Must be the first because compiled by the previous if.
  1003. 'debug',
  1004. 'nocache',
  1005. 'cache',
  1006. 'groups',
  1007. 'as', 'foreach',
  1008. 'if',
  1009. 'call-assets',
  1010. 'lang',
  1011. 'options',
  1012. 'att',
  1013. 'field', 'esc', 'raw', 'out',
  1014. 'tag-open',
  1015. 'call',
  1016. 'set',
  1017. 'inner-content',
  1018. 'tag-close',
  1019. ]
  1020. # compile
  1021. def _compile_node(self, el, compile_context, level):
  1022. """ Compile the given element into python code.
  1023. The t-* attributes (directives) will be converted to a python instruction. If there
  1024. are no t-* attributes, the element will be considered static.
  1025. Directives are compiled using the order provided by the
  1026. ``_directives_eval_order`` method (an create the
  1027. ``compile_context['iter_directives']`` iterator).
  1028. For compilation, the directives supported are those with a
  1029. compilation method ``_compile_directive_*``
  1030. :return: list of string
  1031. """
  1032. # Internal directive used to skip a rendering.
  1033. if 't-qweb-skip' in el.attrib:
  1034. return []
  1035. # if tag don't have qweb attributes don't use directives
  1036. if self._is_static_node(el, compile_context):
  1037. return self._compile_static_node(el, compile_context, level)
  1038. path = compile_context['root'].getpath(el)
  1039. xml = etree.tostring(etree.Element(el.tag, el.attrib), encoding='unicode')
  1040. compile_context['_qweb_error_path_xml'] = (path, xml)
  1041. body = [indent_code(f'# element: {path!r} , {xml!r}', level)]
  1042. # create an iterator on directives to compile in order
  1043. compile_context['iter_directives'] = iter(self._directives_eval_order())
  1044. # add technical directive tag-open, tag-close, inner-content and take
  1045. # care of the namspace
  1046. if not el.nsmap:
  1047. unqualified_el_tag = el_tag = el.tag
  1048. else:
  1049. # Etree will remove the ns prefixes indirection by inlining the corresponding
  1050. # nsmap definition into the tag attribute. Restore the tag and prefix here.
  1051. # Note: we do not support namespace dynamic attributes, we need a default URI
  1052. # on the root and use attribute directive t-att="{'xmlns:example': value}".
  1053. unqualified_el_tag = etree.QName(el.tag).localname
  1054. el_tag = unqualified_el_tag
  1055. if el.prefix:
  1056. el_tag = f'{el.prefix}:{el_tag}'
  1057. if unqualified_el_tag != 't':
  1058. el.set('t-tag-open', el_tag)
  1059. if unqualified_el_tag not in VOID_ELEMENTS:
  1060. el.set('t-tag-close', el_tag)
  1061. if not ({'t-out', 't-esc', 't-raw', 't-field'} & set(el.attrib)):
  1062. el.set('t-inner-content', 'True')
  1063. return body + self._compile_directives(el, compile_context, level)
  1064. def _compile_static_node(self, el, compile_context, level):
  1065. """ Compile a purely static element into a list of string. """
  1066. if not el.nsmap:
  1067. unqualified_el_tag = el_tag = el.tag
  1068. attrib = self._post_processing_att(el.tag, el.attrib)
  1069. else:
  1070. # Etree will remove the ns prefixes indirection by inlining the corresponding
  1071. # nsmap definition into the tag attribute. Restore the tag and prefix here.
  1072. unqualified_el_tag = etree.QName(el.tag).localname
  1073. el_tag = unqualified_el_tag
  1074. if el.prefix:
  1075. el_tag = f'{el.prefix}:{el_tag}'
  1076. attrib = {}
  1077. # If `el` introduced new namespaces, write them as attribute by using the
  1078. # `attrib` dict.
  1079. for ns_prefix, ns_definition in set(el.nsmap.items()) - set(compile_context['nsmap'].items()):
  1080. if ns_prefix is None:
  1081. attrib['xmlns'] = ns_definition
  1082. else:
  1083. attrib[f'xmlns:{ns_prefix}'] = ns_definition
  1084. # Etree will also remove the ns prefixes indirection in the attributes. As we only have
  1085. # the namespace definition, we'll use an nsmap where the keys are the definitions and
  1086. # the values the prefixes in order to get back the right prefix and restore it.
  1087. ns = chain(compile_context['nsmap'].items(), el.nsmap.items())
  1088. nsprefixmap = {v: k for k, v in ns}
  1089. for key, value in el.attrib.items():
  1090. attrib_qname = etree.QName(key)
  1091. if attrib_qname.namespace:
  1092. attrib[f'{nsprefixmap[attrib_qname.namespace]}:{attrib_qname.localname}'] = value
  1093. else:
  1094. attrib[key] = value
  1095. attrib = self._post_processing_att(el.tag, attrib)
  1096. # Update the dict of inherited namespaces before continuing the recursion. Note:
  1097. # since `compile_context['nsmap']` is a dict (and therefore mutable) and we do **not**
  1098. # want changes done in deeper recursion to bevisible in earlier ones, we'll pass
  1099. # a copy before continuing the recursion and restore the original afterwards.
  1100. original_nsmap = dict(compile_context['nsmap'])
  1101. if unqualified_el_tag != 't':
  1102. attributes = ''.join(f' {name}="{escape(str(value))}"'
  1103. for name, value in attrib.items() if value or isinstance(value, str))
  1104. self._append_text(f'<{el_tag}{"".join(attributes)}', compile_context)
  1105. if unqualified_el_tag in VOID_ELEMENTS:
  1106. self._append_text('/>', compile_context)
  1107. else:
  1108. self._append_text('>', compile_context)
  1109. el.attrib.clear()
  1110. if el.nsmap:
  1111. compile_context['nsmap'].update(el.nsmap)
  1112. body = self._compile_directive(el, compile_context, 'inner-content', level)
  1113. compile_context['nsmap'] = original_nsmap
  1114. else:
  1115. body = self._compile_directive(el, compile_context, 'inner-content', level)
  1116. if unqualified_el_tag != 't':
  1117. if unqualified_el_tag not in VOID_ELEMENTS:
  1118. self._append_text(f'</{el_tag}>', compile_context)
  1119. return body
  1120. def _compile_directives(self, el, compile_context, level):
  1121. """ Compile the given element, following the directives given in the
  1122. iterator ``compile_context['iter_directives']`` create by
  1123. `_compile_node`` method.
  1124. :return: list of code lines
  1125. """
  1126. if self._is_static_node(el, compile_context):
  1127. el.attrib.pop('t-tag-open', None)
  1128. el.attrib.pop('t-inner-content', None)
  1129. el.attrib.pop('t-tag-close', None)
  1130. return self._compile_static_node(el, compile_context, level)
  1131. code = []
  1132. # compile the directives still present on the element
  1133. for directive in compile_context['iter_directives']:
  1134. if ('t-' + directive) in el.attrib:
  1135. code.extend(self._compile_directive(el, compile_context, directive, level))
  1136. elif directive == 'groups':
  1137. if directive in el.attrib:
  1138. code.extend(self._compile_directive(el, compile_context, directive, level))
  1139. elif directive == 'att':
  1140. code.extend(self._compile_directive(el, compile_context, directive, level))
  1141. elif directive == 'options':
  1142. if any(name.startswith('t-options-') for name in el.attrib):
  1143. code.extend(self._compile_directive(el, compile_context, directive, level))
  1144. elif directive == 'nocache':
  1145. if any(name.startswith('t-nocache-') for name in el.attrib):
  1146. code.extend(self._compile_directive(el, compile_context, directive, level))
  1147. # compile unordered directives still present on the element
  1148. for att in el.attrib:
  1149. if att not in SPECIAL_DIRECTIVES and att.startswith('t-') and getattr(self, f"_compile_directive_{att[2:].replace('-', '_')}", None):
  1150. code.extend(self._compile_directive(el, compile_context, directive, level))
  1151. remaining = set(el.attrib) - SPECIAL_DIRECTIVES
  1152. if remaining:
  1153. _logger.warning('Unknown directives or unused attributes: %s in %s', remaining, compile_context['template'])
  1154. return code
  1155. @QwebTracker.wrap_compile_directive
  1156. def _compile_directive(self, el, compile_context, directive, level):
  1157. compile_handler = getattr(self, f"_compile_directive_{directive.replace('-', '_')}", None)
  1158. return compile_handler(el, compile_context, level)
  1159. # compile directives
  1160. def _compile_directive_debug(self, el, compile_context, level):
  1161. """Compile `t-debug` expressions into a python code as a list of
  1162. strings.
  1163. The code will contains the call to the debugger chosen from the valid
  1164. list.
  1165. """
  1166. debugger = el.attrib.pop('t-debug')
  1167. code = []
  1168. if compile_context.get('dev_mode'):
  1169. code.append(indent_code(f"self._debug_trace({debugger!r}, values)", level))
  1170. else:
  1171. _logger.warning("@t-debug in template is only available in qweb dev mode")
  1172. return code
  1173. def _compile_directive_options(self, el, compile_context, level):
  1174. """
  1175. compile t-options and add to the dict the t-options-xxx. Will create
  1176. the dictionary ``values['__qweb_options__']`` in compiled code.
  1177. """
  1178. code = []
  1179. dict_options = []
  1180. for key in list(el.attrib):
  1181. if key.startswith('t-options-'):
  1182. value = el.attrib.pop(key)
  1183. option_name = key[10:]
  1184. dict_options.append(f'{option_name!r}:{self._compile_expr(value)}')
  1185. t_options = el.attrib.pop('t-options', None)
  1186. if t_options and dict_options:
  1187. code.append(indent_code(f"values['__qweb_options__'] = {{**{self._compile_expr(t_options)}, {', '.join(dict_options)}}}", level))
  1188. elif dict_options:
  1189. code.append(indent_code(f"values['__qweb_options__'] = {{{', '.join(dict_options)}}}", level))
  1190. elif t_options:
  1191. code.append(indent_code(f"values['__qweb_options__'] = {self._compile_expr(t_options)}", level))
  1192. else:
  1193. code.append(indent_code("values['__qweb_options__'] = {}", level))
  1194. el.set('t-consumed-options', str(bool(code)))
  1195. return code
  1196. def _compile_directive_consumed_options(self, el, compile_context, level):
  1197. raise SyntaxError('the t-options must be on the same tag as a directive that consumes it (for example: t-out, t-field, t-call)')
  1198. def _compile_directive_att(self, el, compile_context, level):
  1199. """ Compile the attributes of the given elements.
  1200. The compiled function will create the ``values['__qweb_attrs__']``
  1201. dictionary. Then the dictionary will be output.
  1202. The new namespaces of the current element.
  1203. The static attributes (not prefixed by ``t-``) are add to the
  1204. dictionary in first.
  1205. The dynamic attributes values will be add after. The dynamic
  1206. attributes has different origins.
  1207. - value from key equal to ``t-att``: python dictionary expression;
  1208. - value from keys that start with ``t-att-``: python expression;
  1209. - value from keys that start with ``t-attf-``: format string
  1210. expression.
  1211. """
  1212. code = [indent_code("attrs = values['__qweb_attrs__'] = {}", level)]
  1213. # Compile the introduced new namespaces of the given element.
  1214. #
  1215. # Add the found new attributes into the `attrs` dictionary like
  1216. # the static attributes.
  1217. if el.nsmap:
  1218. for ns_prefix, ns_definition in set(el.nsmap.items()) - set(compile_context['nsmap'].items()):
  1219. key = 'xmlns'
  1220. if ns_prefix is not None:
  1221. key = f'xmlns:{ns_prefix}'
  1222. code.append(indent_code(f'attrs[{key!r}] = {ns_definition!r}', level))
  1223. # Compile the static attributes of the given element.
  1224. #
  1225. # Etree will also remove the ns prefixes indirection in the
  1226. # attributes. As we only have the namespace definition, we'll use
  1227. # an nsmap where the keys are the definitions and the values the
  1228. # prefixes in order to get back the right prefix and restore it.
  1229. if any(not name.startswith('t-') for name in el.attrib):
  1230. nsprefixmap = {v: k for k, v in chain(compile_context['nsmap'].items(), el.nsmap.items())}
  1231. for key in list(el.attrib):
  1232. if not key.startswith('t-'):
  1233. value = el.attrib.pop(key)
  1234. attrib_qname = etree.QName(key)
  1235. if attrib_qname.namespace:
  1236. key = f'{nsprefixmap[attrib_qname.namespace]}:{attrib_qname.localname}'
  1237. code.append(indent_code(f'attrs[{key!r}] = {value!r}', level))
  1238. # Compile the dynamic attributes of the given element. All
  1239. # attributes will be add to the ``attrs`` dictionary in the
  1240. # compiled function.
  1241. for key in list(el.attrib):
  1242. if key.startswith('t-attf-'):
  1243. value = el.attrib.pop(key)
  1244. code.append(indent_code(f"attrs[{key[7:]!r}] = {self._compile_format(value)}", level))
  1245. elif key.startswith('t-att-'):
  1246. value = el.attrib.pop(key)
  1247. code.append(indent_code(f"attrs[{key[6:]!r}] = {self._compile_expr(value)}", level))
  1248. elif key == 't-att':
  1249. value = el.attrib.pop(key)
  1250. code.append(indent_code(f"""
  1251. atts_value = {self._compile_expr(value)}
  1252. if isinstance(atts_value, dict):
  1253. attrs.update(atts_value)
  1254. elif isinstance(atts_value, (list, tuple)) and not isinstance(atts_value[0], (list, tuple)):
  1255. attrs.update([atts_value])
  1256. elif isinstance(atts_value, (list, tuple)):
  1257. attrs.update(dict(atts_value))
  1258. """, level))
  1259. return code
  1260. def _compile_directive_tag_open(self, el, compile_context, level):
  1261. """ Compile the opening tag with attributes of the given element into
  1262. a list of python code line.
  1263. The compiled function will fill the ``attrs`` dictionary. Then the
  1264. ``attrs`` dictionary will be output and reset the value of ``attrs``.
  1265. The static attributes (not prefixed by ``t-``) are add to the
  1266. ``attrs`` dictionary in first.
  1267. The dynamic attributes values will be add after. The dynamic
  1268. attributes has different origins.
  1269. - value from key equal to ``t-att``: python dictionary expression;
  1270. - value from keys that start with ``t-att-``: python expression;
  1271. - value from keys that start with ``t-attf-``: format string
  1272. expression.
  1273. """
  1274. el_tag = el.attrib.pop('t-tag-open', None)
  1275. if not el_tag:
  1276. return []
  1277. # open the open tag
  1278. self._append_text(f"<{el_tag}", compile_context)
  1279. code = self._flush_text(compile_context, level)
  1280. # Generates the part of the code that prost process and output the
  1281. # attributes from ``attrs`` dictionary. Consumes `attrs` dictionary
  1282. # and reset it.
  1283. #
  1284. # Use str(value) to change Markup into str and escape it, then use str
  1285. # to avoid the escaping of the other html content.
  1286. code.append(indent_code(f"""
  1287. attrs = values.pop('__qweb_attrs__', None)
  1288. if attrs:
  1289. tagName = {el.tag!r}
  1290. attrs = self._post_processing_att(tagName, attrs)
  1291. for name, value in attrs.items():
  1292. if value or isinstance(value, str):
  1293. yield f' {{escape(str(name))}}="{{escape(str(value))}}"'
  1294. """, level))
  1295. # close the open tag
  1296. if 't-tag-close' in el.attrib:
  1297. self._append_text('>', compile_context)
  1298. else:
  1299. self._append_text('/>', compile_context)
  1300. return code
  1301. def _compile_directive_tag_close(self, el, compile_context, level):
  1302. """ Compile the closing tag of the given element into string.
  1303. Returns an empty list because it's use only `_append_text`.
  1304. """
  1305. el_tag = el.attrib.pop("t-tag-close", None)
  1306. if el_tag:
  1307. self._append_text(f'</{el_tag}>', compile_context)
  1308. return []
  1309. def _compile_directive_set(self, el, compile_context, level):
  1310. """Compile `t-set` expressions into a python code as a list of
  1311. strings.
  1312. There are 3 kinds of `t-set`:
  1313. * `t-value` containing python code;
  1314. * `t-valuef` containing strings to format;
  1315. * whose value is the content of the tag (being Markup safe).
  1316. The code will contain the assignment of the dynamically generated value.
  1317. """
  1318. code = self._flush_text(compile_context, level, rstrip=el.tag.lower() == 't')
  1319. if 't-set' in el.attrib:
  1320. varname = el.attrib.pop('t-set')
  1321. if varname == "":
  1322. raise KeyError('t-set')
  1323. if varname != T_CALL_SLOT and varname[0] != '{' and not VARNAME_REGEXP.match(varname):
  1324. raise ValueError('The varname can only contain alphanumeric characters and underscores.')
  1325. if 't-value' in el.attrib or 't-valuef' in el.attrib or varname[0] == '{':
  1326. el.attrib.pop('t-inner-content') # The content is considered empty.
  1327. if varname == T_CALL_SLOT:
  1328. raise SyntaxError('t-set="0" should not be set from t-value or t-valuef')
  1329. if 't-value' in el.attrib:
  1330. expr = el.attrib.pop('t-value') or 'None'
  1331. code.append(indent_code(f"values[{varname!r}] = {self._compile_expr(expr)}", level))
  1332. elif 't-valuef' in el.attrib:
  1333. exprf = el.attrib.pop('t-valuef')
  1334. code.append(indent_code(f"values[{varname!r}] = {self._compile_format(exprf)}", level))
  1335. elif varname[0] == '{':
  1336. code.append(indent_code(f"values.update({self._compile_expr(varname)})", level))
  1337. else:
  1338. # set the content as value
  1339. content = (
  1340. self._compile_directive(el, compile_context, 'inner-content', 1) +
  1341. self._flush_text(compile_context, 1))
  1342. if content:
  1343. def_name = compile_context['make_name']('t_set')
  1344. compile_context['template_functions'][def_name] = [f"def {def_name}(self, values):"] + content
  1345. code.append(indent_code(f"""
  1346. t_set = []
  1347. for item in {def_name}(self, values):
  1348. if isinstance(item, str):
  1349. t_set.append(item)
  1350. else:
  1351. ref, function_name, cached_values = item
  1352. t_nocache_function = values['__qweb_loaded_values'].get(function_name)
  1353. if not t_nocache_function:
  1354. t_call_template_functions, def_name = self._compile(ref)
  1355. t_nocache_function = t_call_template_functions[function_name]
  1356. nocache_values = values['__qweb_root_values'].copy()
  1357. nocache_values.update(cached_values)
  1358. t_set.extend(t_nocache_function(self, nocache_values))
  1359. """, level))
  1360. expr = "Markup(''.join(t_set))"
  1361. else:
  1362. expr = "''"
  1363. code.append(indent_code(f"values[{varname!r}] = {expr}", level))
  1364. return code
  1365. def _compile_directive_value(self, el, compile_context, level):
  1366. """Compile `t-value` expressions into a python code as a list of strings.
  1367. This method only check if this attributes is on the same node of a
  1368. `t-set` attribute.
  1369. """
  1370. raise SyntaxError("t-value must be on the same node of t-set")
  1371. def _compile_directive_valuef(self, el, compile_context, level):
  1372. """Compile `t-valuef` expressions into a python code as a list of strings.
  1373. This method only check if this attributes is on the same node of a
  1374. `t-set` attribute.
  1375. """
  1376. raise SyntaxError("t-valuef must be on the same node of t-set")
  1377. def _compile_directive_inner_content(self, el, compile_context, level):
  1378. """Compiles the content of the element (is the technical `t-inner-content`
  1379. directive created by QWeb) into a python code as a list of
  1380. strings.
  1381. The code will contains the text content of the node or the compliled
  1382. code from the recursive call of ``_compile_node``.
  1383. """
  1384. el.attrib.pop('t-inner-content', None)
  1385. if el.nsmap:
  1386. # Update the dict of inherited namespaces before continuing the recursion. Note:
  1387. # since `compile_context['nsmap']` is a dict (and therefore mutable) and we do **not**
  1388. # want changes done in deeper recursion to bevisible in earlier ones, we'll pass
  1389. # a copy before continuing the recursion and restore the original afterwards.
  1390. compile_context = dict(compile_context, nsmap=el.nsmap)
  1391. if el.text is not None:
  1392. self._append_text(el.text, compile_context)
  1393. body = []
  1394. for item in el:
  1395. if isinstance(item, etree._Comment):
  1396. if compile_context.get('preserve_comments'):
  1397. self._append_text(f"<!--{item.text}-->", compile_context)
  1398. elif isinstance(item, etree._ProcessingInstruction):
  1399. if compile_context.get('preserve_comments'):
  1400. self._append_text(f"<?{item.target} {item.text}?>", compile_context)
  1401. else:
  1402. body.extend(self._compile_node(item, compile_context, level))
  1403. # comments can also contains tail text
  1404. if item.tail is not None:
  1405. self._append_text(item.tail, compile_context)
  1406. return body
  1407. def _compile_directive_if(self, el, compile_context, level):
  1408. """Compile `t-if` expressions into a python code as a list of strings.
  1409. The code will contain the condition `if`, `else` and `elif` part that
  1410. wrap the rest of the compiled code of this element.
  1411. """
  1412. expr = el.attrib.pop('t-if', el.attrib.pop('t-elif', None))
  1413. assert not expr.isspace(), 't-if or t-elif expression should not be empty.'
  1414. strip = self._rstrip_text(compile_context) # the withspaces is visible only when display a content
  1415. if el.tag.lower() == 't' and el.text and LSTRIP_REGEXP.search(el.text):
  1416. strip = '' # remove technical spaces
  1417. code = self._flush_text(compile_context, level)
  1418. code.append(indent_code(f"if {self._compile_expr(expr)}:", level))
  1419. body = []
  1420. if strip:
  1421. self._append_text(strip, compile_context)
  1422. body.extend(
  1423. self._compile_directives(el, compile_context, level + 1) +
  1424. self._flush_text(compile_context, level + 1, rstrip=True))
  1425. code.extend(body or [indent_code('pass', level + 1)])
  1426. # Look for the else or elif conditions
  1427. next_el = el.getnext()
  1428. comments_to_remove = []
  1429. while isinstance(next_el, etree._Comment):
  1430. comments_to_remove.append(next_el)
  1431. next_el = next_el.getnext()
  1432. # If there is a t-else directive, the comment nodes are deleted
  1433. # and the t-else or t-elif is validated.
  1434. if next_el is not None and {'t-else', 't-elif'} & set(next_el.attrib):
  1435. # Insert a flag to allow t-else or t-elif rendering.
  1436. next_el.attrib['t-else-valid'] = 'True'
  1437. # remove comment node
  1438. parent = el.getparent()
  1439. for comment in comments_to_remove:
  1440. parent.remove(comment)
  1441. if el.tail and not el.tail.isspace():
  1442. raise SyntaxError("Unexpected non-whitespace characters between t-if and t-else directives")
  1443. el.tail = None
  1444. # You have to render the `t-else` and `t-elif` here in order
  1445. # to be able to put the log. Otherwise, the parent's
  1446. # `t-inner-content`` directive will render the different
  1447. # nodes without taking indentation into account such as:
  1448. # if (if_expression):
  1449. # content_if
  1450. # log ['last_path_node'] = path
  1451. # else:
  1452. # content_else
  1453. code.append(indent_code("else:", level))
  1454. body = []
  1455. if strip:
  1456. self._append_text(strip, compile_context)
  1457. body.extend(
  1458. self._compile_node(next_el, compile_context, level + 1)+
  1459. self._flush_text(compile_context, level + 1, rstrip=True))
  1460. code.extend(body or [indent_code('pass', level + 1)])
  1461. # Insert a flag to avoid the t-else or t-elif rendering when
  1462. # the parent t-inner-content dirrective compile his
  1463. # children.
  1464. next_el.attrib['t-qweb-skip'] = 'True'
  1465. return code
  1466. def _compile_directive_elif(self, el, compile_context, level):
  1467. """Compile `t-elif` expressions into a python code as a list of
  1468. strings. This method is linked with the `t-if` directive.
  1469. Check if this directive is valide, the t-qweb-skip flag and call
  1470. `t-if` directive
  1471. """
  1472. if not el.attrib.pop('t-else-valid', None):
  1473. raise SyntaxError("t-elif directive must be preceded by t-if or t-elif directive")
  1474. return self._compile_directive_if(el, compile_context, level)
  1475. def _compile_directive_else(self, el, compile_context, level):
  1476. """Compile `t-else` expressions into a python code as a list of strings.
  1477. This method is linked with the `t-if` directive.
  1478. Check if this directive is valide and add the t-qweb-skip flag.
  1479. """
  1480. if not el.attrib.pop('t-else-valid', None):
  1481. raise SyntaxError("t-elif directive must be preceded by t-if or t-elif directive")
  1482. el.attrib.pop('t-else')
  1483. return []
  1484. def _compile_directive_groups(self, el, compile_context, level):
  1485. """Compile `t-groups` expressions into a python code as a list of
  1486. strings.
  1487. The code will contain the condition `if self.user_has_groups(groups)`
  1488. part that wrap the rest of the compiled code of this element.
  1489. """
  1490. groups = el.attrib.pop('t-groups', el.attrib.pop('groups', None))
  1491. strip = self._rstrip_text(compile_context)
  1492. code = self._flush_text(compile_context, level)
  1493. code.append(indent_code(f"if self.user_has_groups({groups!r}):", level))
  1494. if strip and el.tag.lower() != 't':
  1495. self._append_text(strip, compile_context)
  1496. code.extend([
  1497. *self._compile_directives(el, compile_context, level + 1),
  1498. *self._flush_text(compile_context, level + 1, rstrip=True),
  1499. ] or [indent_code('pass', level + 1)])
  1500. return code
  1501. def _compile_directive_foreach(self, el, compile_context, level):
  1502. """Compile `t-foreach` expressions into a python code as a list of
  1503. strings.
  1504. `t-as` is used to define the key name.
  1505. `t-foreach` compiled value can be an iterable, an dictionary or a
  1506. number.
  1507. The code will contain loop `for` that wrap the rest of the compiled
  1508. code of this element.
  1509. Some key into values dictionary are create automatically:
  1510. *_size, *_index, *_value, *_first, *_last, *_odd, *_even, *_parity
  1511. """
  1512. expr_foreach = el.attrib.pop('t-foreach')
  1513. expr_as = el.attrib.pop('t-as')
  1514. if not expr_as:
  1515. raise KeyError('t-as')
  1516. if not VARNAME_REGEXP.match(expr_as):
  1517. raise ValueError(f'The varname {expr_as!r} can only contain alphanumeric characters and underscores.')
  1518. if el.tag.lower() == 't':
  1519. self._rstrip_text(compile_context)
  1520. code = self._flush_text(compile_context, level)
  1521. content_foreach = (
  1522. self._compile_directives(el, compile_context, level + 1) +
  1523. self._flush_text(compile_context, level + 1, rstrip=True))
  1524. t_foreach = compile_context['make_name']('t_foreach')
  1525. size = compile_context['make_name']('size')
  1526. has_value = compile_context['make_name']('has_value')
  1527. if expr_foreach.isdigit():
  1528. code.append(indent_code(f"""
  1529. values[{expr_as + '_size'!r}] = {size} = {int(expr_foreach)}
  1530. {t_foreach} = range({size})
  1531. {has_value} = False
  1532. """, level))
  1533. else:
  1534. code.append(indent_code(f"""
  1535. {t_foreach} = {self._compile_expr(expr_foreach)} or []
  1536. if isinstance({t_foreach}, Sized):
  1537. values[{expr_as + '_size'!r}] = {size} = len({t_foreach})
  1538. elif ({t_foreach}).__class__ == int:
  1539. values[{expr_as + '_size'!r}] = {size} = {t_foreach}
  1540. {t_foreach} = range({size})
  1541. else:
  1542. {size} = None
  1543. {has_value} = False
  1544. if isinstance({t_foreach}, Mapping):
  1545. {t_foreach} = {t_foreach}.items()
  1546. {has_value} = True
  1547. """, level))
  1548. code.append(indent_code(f"""
  1549. for index, item in enumerate({t_foreach}):
  1550. values[{expr_as + '_index'!r}] = index
  1551. if {has_value}:
  1552. values[{expr_as!r}], values[{expr_as + '_value'!r}] = item
  1553. else:
  1554. values[{expr_as!r}] = values[{expr_as + '_value'!r}] = item
  1555. values[{expr_as + '_first'!r}] = values[{expr_as + '_index'!r}] == 0
  1556. if {size} is not None:
  1557. values[{expr_as + '_last'!r}] = index + 1 == {size}
  1558. values[{expr_as + '_odd'!r}] = index % 2
  1559. values[{expr_as + '_even'!r}] = not values[{expr_as + '_odd'!r}]
  1560. values[{expr_as + '_parity'!r}] = 'odd' if values[{expr_as + '_odd'!r}] else 'even'
  1561. """, level))
  1562. code.extend(content_foreach or indent_code('continue', level + 1))
  1563. return code
  1564. def _compile_directive_as(self, el, compile_context, level):
  1565. """Compile `t-as` expressions into a python code as a list of strings.
  1566. This method only check if this attributes is on the same node of a
  1567. `t-foreach` attribute.
  1568. """
  1569. if 't-foreach' not in el.attrib:
  1570. raise SyntaxError("t-as must be on the same node of t-foreach")
  1571. return []
  1572. def _compile_directive_out(self, el, compile_context, level):
  1573. """Compile `t-out` expressions into a python code as a list of
  1574. strings.
  1575. The code will contain evalution and rendering of the compiled value. If
  1576. the compiled value is None or False, the tag is not added to the render
  1577. (Except if the widget forces rendering or there is default content).
  1578. (eg: `<t t-out="my_value">Default content if falsy</t>`)
  1579. The output can have some rendering option with `t-options-widget` or
  1580. `t-options={'widget': ...}. At rendering time, The compiled code will
  1581. call ``_get_widget`` method or ``_get_field`` method for `t-field`.
  1582. A `t-field` will necessarily be linked to the value of a record field
  1583. (eg: `<span t-field="record.field_name"/>`), a t-out` can be applied
  1584. to any value (eg: `<span t-out="10" t-options-widget="'float'"/>`).
  1585. """
  1586. ttype = 't-out'
  1587. expr = el.attrib.pop('t-out', None)
  1588. if expr is None:
  1589. ttype = 't-field'
  1590. expr = el.attrib.pop('t-field', None)
  1591. if expr is None:
  1592. # deprecated use.
  1593. ttype = 't-esc'
  1594. expr = el.attrib.pop('t-esc', None)
  1595. if expr is None:
  1596. ttype = 't-raw'
  1597. expr = el.attrib.pop('t-raw')
  1598. code = self._flush_text(compile_context, level)
  1599. code_options = el.attrib.pop('t-consumed-options', 'None')
  1600. tag_open = (
  1601. self._compile_directive(el, compile_context, 'tag-open', level + 1) +
  1602. self._flush_text(compile_context, level + 1))
  1603. tag_close = (
  1604. self._compile_directive(el, compile_context, 'tag-close', level + 1) +
  1605. self._flush_text(compile_context, level + 1))
  1606. default_body = (
  1607. self._compile_directive(el, compile_context, 'inner-content', level + 1) +
  1608. self._flush_text(compile_context, level + 1))
  1609. # The generated code will set the values of the content, attrs (used to
  1610. # output attributes) and the force_display (if the widget or field
  1611. # mark force_display as True, the tag will be inserted in the output
  1612. # even the value of content is None and without default value)
  1613. if expr == T_CALL_SLOT and code_options != 'True':
  1614. code.append(indent_code("if True:", level))
  1615. code.extend(tag_open)
  1616. code.append(indent_code(f"yield from values.get({T_CALL_SLOT}, [])", level + 1))
  1617. code.extend(tag_close)
  1618. return code
  1619. elif ttype == 't-field':
  1620. record, field_name = expr.rsplit('.', 1)
  1621. code.append(indent_code(f"""
  1622. field_attrs, content, force_display = self._get_field({self._compile_expr(record, raise_on_missing=True)}, {field_name!r}, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), values)
  1623. if values.get('__qweb_attrs__') is None:
  1624. values['__qweb_attrs__'] = field_attrs
  1625. else:
  1626. values['__qweb_attrs__'].update(field_attrs)
  1627. if content is not None and content is not False:
  1628. content = self._compile_to_str(content)
  1629. """, level))
  1630. force_display_dependent = True
  1631. else:
  1632. if expr == T_CALL_SLOT:
  1633. code.append(indent_code(f"content = Markup(''.join(values.get({T_CALL_SLOT}, [])))", level))
  1634. else:
  1635. code.append(indent_code(f"content = {self._compile_expr(expr)}", level))
  1636. if code_options == 'True':
  1637. code.append(indent_code(f"""
  1638. widget_attrs, content, force_display = self._get_widget(content, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), values)
  1639. if values.get('__qweb_attrs__') is None:
  1640. values['__qweb_attrs__'] = widget_attrs
  1641. else:
  1642. values['__qweb_attrs__'].update(widget_attrs)
  1643. content = self._compile_to_str(content)
  1644. """, level))
  1645. force_display_dependent = True
  1646. else:
  1647. force_display_dependent = False
  1648. if ttype == 't-raw':
  1649. # deprecated use.
  1650. code.append(indent_code("""
  1651. if content is not None and content is not False:
  1652. content = Markup(content)
  1653. """, level))
  1654. # The generated code will create the output tag with all attribute.
  1655. # If the value is not falsy or if there is default content or if it's
  1656. # in force_display mode, the tag is add into the output.
  1657. el.attrib.pop('t-tag', None) # code generating the output is done here
  1658. # generate code to display the tag if the value is not Falsy
  1659. code.append(indent_code("if content is not None and content is not False:", level))
  1660. code.extend(tag_open)
  1661. # Use str to avoid the escaping of the other html content because the
  1662. # yield generator MarkupSafe values will be join into an string in
  1663. # `_render`.
  1664. code.append(indent_code("yield str(escape(content))", level + 1))
  1665. code.extend(tag_close)
  1666. # generate code to display the tag with default content if the value is
  1667. # Falsy
  1668. if default_body or compile_context['_text_concat']:
  1669. _text_concat = list(compile_context['_text_concat'])
  1670. compile_context['_text_concat'].clear()
  1671. code.append(indent_code("else:", level))
  1672. code.extend(tag_open)
  1673. code.extend(default_body)
  1674. compile_context['_text_concat'].extend(_text_concat)
  1675. code.extend(tag_close)
  1676. elif force_display_dependent:
  1677. # generate code to display the tag if it's the force_diplay mode.
  1678. if tag_open + tag_close:
  1679. code.append(indent_code("elif force_display:", level))
  1680. code.extend(tag_open + tag_close)
  1681. code.append(indent_code("""else: values.pop('__qweb_attrs__', None)""", level))
  1682. return code
  1683. def _compile_directive_esc(self, el, compile_context, level):
  1684. # deprecated use.
  1685. if compile_context.get('dev_mode'):
  1686. _logger.warning(
  1687. "Found deprecated directive @t-esc=%r in template %r. Replace by @t-out",
  1688. el.get('t-esc'),
  1689. compile_context.get('ref', '<unknown>'),
  1690. )
  1691. return self._compile_directive_out(el, compile_context, level)
  1692. def _compile_directive_raw(self, el, compile_context, level):
  1693. # deprecated use.
  1694. _logger.warning(
  1695. "Found deprecated directive @t-raw=%r in template %r. Replace by "
  1696. "@t-out, and explicitely wrap content in `Markup` if "
  1697. "necessary (which likely is not the case)",
  1698. el.get('t-raw'),
  1699. compile_context.get('ref', '<unknown>'),
  1700. )
  1701. return self._compile_directive_out(el, compile_context, level)
  1702. def _compile_directive_field(self, el, compile_context, level):
  1703. """Compile `t-field` expressions into a python code as a list of
  1704. strings.
  1705. The compiled code will call ``_get_field`` method at rendering time
  1706. using the type of value supplied by the field. This behavior can be
  1707. changed with `t-options-widget` or `t-options={'widget': ...}.
  1708. The code will contain evalution and rendering of the compiled value
  1709. value from the record field. If the compiled value is None or False,
  1710. the tag is not added to the render
  1711. (Except if the widget forces rendering or there is default content.).
  1712. """
  1713. tagName = el.tag
  1714. assert tagName not in ("table", "tbody", "thead", "tfoot", "tr", "td",
  1715. "li", "ul", "ol", "dl", "dt", "dd"),\
  1716. "QWeb widgets do not work correctly on %r elements" % tagName
  1717. assert tagName != 't',\
  1718. "t-field can not be used on a t element, provide an actual HTML node"
  1719. assert "." in el.get('t-field'),\
  1720. "t-field must have at least a dot like 'record.field_name'"
  1721. return self._compile_directive_out(el, compile_context, level)
  1722. def _compile_directive_call(self, el, compile_context, level):
  1723. """Compile `t-call` expressions into a python code as a list of
  1724. strings.
  1725. `t-call` allow formating string dynamic at rendering time.
  1726. Can use `t-options` used to call and render the sub-template at
  1727. rendering time.
  1728. The sub-template is called with a copy of the rendering values
  1729. dictionary. The dictionary contains the key 0 coming from the
  1730. compilation of the contents of this element
  1731. The code will contain the call of the template and a function from the
  1732. compilation of the content of this element.
  1733. """
  1734. expr = el.attrib.pop('t-call')
  1735. if el.attrib.get('t-call-options'): # retro-compatibility
  1736. el.attrib.set('t-options', el.attrib.pop('t-call-options'))
  1737. nsmap = compile_context.get('nsmap')
  1738. code = self._flush_text(compile_context, level, rstrip=el.tag.lower() == 't')
  1739. # options
  1740. el.attrib.pop('t-consumed-options', None)
  1741. code.append(indent_code("t_call_options = values.pop('__qweb_options__', {})", level))
  1742. if nsmap:
  1743. # update this dict with the current nsmap so that the callee know
  1744. # if he outputting the xmlns attributes is relevenat or not
  1745. nsmap = []
  1746. for key, value in compile_context['nsmap'].items():
  1747. if isinstance(key, str):
  1748. nsmap.append(f'{key!r}:{value!r}')
  1749. else:
  1750. nsmap.append(f'None:{value!r}')
  1751. code.append(indent_code(f"t_call_options.update(nsmap={{{', '.join(nsmap)}}})", level))
  1752. # values (t-out="0" from content and variables from t-set)
  1753. def_name = compile_context['make_name']('t_call')
  1754. # values from content (t-out="0" and t-set inside the content)
  1755. code_content = [f"def {def_name}(self, values):"]
  1756. code_content.extend(self._compile_directive(el, compile_context, 'inner-content', 1))
  1757. self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
  1758. code_content.extend(self._flush_text(compile_context, 1, rstrip=True))
  1759. compile_context['template_functions'][def_name] = code_content
  1760. code.append(indent_code(f"""
  1761. t_call_values = values.copy()
  1762. t_call_values[{T_CALL_SLOT}] = list({def_name}(self, t_call_values))
  1763. """, level))
  1764. template = self._compile_format(expr)
  1765. # call
  1766. code.append(indent_code(f"""
  1767. irQweb = self.with_context(**t_call_options)
  1768. template = {template}
  1769. if template.isnumeric():
  1770. template = int(template)
  1771. t_call_template_functions, def_name = irQweb._compile(template)
  1772. render_template = t_call_template_functions[def_name]
  1773. yield from render_template(irQweb, t_call_values)
  1774. """, level))
  1775. return code
  1776. def _compile_directive_lang(self, el, compile_context, level):
  1777. if 't-call' not in el.attrib:
  1778. raise SyntaxError("t-lang is an alias of t-options-lang but only available on the same node of t-call")
  1779. el.attrib['t-options-lang'] = el.attrib.pop('t-lang')
  1780. return self._compile_node(el, compile_context, level)
  1781. def _compile_directive_call_assets(self, el, compile_context, level):
  1782. """ This special 't-call-assets' tag can be used in order to aggregate/minify javascript and css assets"""
  1783. if len(el) > 0:
  1784. raise SyntaxError("t-call-assets cannot contain children nodes")
  1785. code = self._flush_text(compile_context, level)
  1786. xmlid = el.attrib.pop('t-call-assets')
  1787. css = self._compile_bool(el.attrib.pop('t-css', True))
  1788. js = self._compile_bool(el.attrib.pop('t-js', True))
  1789. async_load = self._compile_bool(el.attrib.pop('async_load', False))
  1790. defer_load = self._compile_bool(el.attrib.pop('defer_load', False))
  1791. lazy_load = self._compile_bool(el.attrib.pop('lazy_load', False))
  1792. media = el.attrib.pop('media', False)
  1793. code.append(indent_code(f"""
  1794. t_call_assets_nodes = self._get_asset_nodes(
  1795. {xmlid!r},
  1796. css={css},
  1797. js={js},
  1798. debug=values.get("debug"),
  1799. async_load={async_load},
  1800. defer_load={defer_load},
  1801. lazy_load={lazy_load},
  1802. media={media!r},
  1803. )
  1804. """.strip(), level))
  1805. code.append(indent_code("""
  1806. for index, (tagName, asset_attrs, content) in enumerate(t_call_assets_nodes):
  1807. if index:
  1808. yield '\\n '
  1809. yield '<'
  1810. yield tagName
  1811. attrs = self._post_processing_att(tagName, asset_attrs)
  1812. for name, value in attrs.items():
  1813. if value or isinstance(value, str):
  1814. yield f' {escape(str(name))}="{escape(str(value))}"'
  1815. if not content and tagName in VOID_ELEMENTS:
  1816. yield '/>'
  1817. else:
  1818. yield '>'
  1819. if content:
  1820. yield content
  1821. yield '</'
  1822. yield tagName
  1823. yield '>'
  1824. """, level))
  1825. return code
  1826. def _compile_directive_cache(self, el, compile_context, level):
  1827. """Compile the `t-cache` tuple expression into a key cache.
  1828. The `t-cache` directive allows you to keep the rendered result
  1829. of a template part. The supplied key must be a tuple. This tuple
  1830. can contain recordset in this case the zone will be invalidated
  1831. each time the write_date of these records changes.
  1832. The values are scoped into the `t-cache` and are not available
  1833. outside.
  1834. see: `t-nocache`
  1835. """
  1836. expr = el.attrib.pop('t-cache')
  1837. code = self._flush_text(compile_context, level)
  1838. def_name = compile_context['make_name']('t_cache')
  1839. # Generate the content function
  1840. def_code = [indent_code(f"""def {def_name}(self, values):""", 0)]
  1841. def_content = self._compile_directives(el, compile_context, 1)
  1842. if def_content and not compile_context['_text_concat']:
  1843. self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
  1844. def_code.extend(def_content)
  1845. def_code.extend(self._flush_text(compile_context, 1))
  1846. compile_context['template_functions'][def_name] = def_code
  1847. # Get the dynamic key for the cache and load the content.
  1848. # The t-nocache yield a tuple (ref, function name) instead of a
  1849. # When reading tuple coming from t-nocache, we check if the
  1850. # method is already known otherwise the corresponding template
  1851. # and its functions are loaded.
  1852. code.append(indent_code(f"""
  1853. template_cache_key = {self._compile_expr(expr)} if not self.env.context.get('is_t_cache_disabled') else None
  1854. cache_key = self._get_cache_key(template_cache_key) if template_cache_key else None
  1855. uniq_cache_key = cache_key and ({str(self.env.context['__qweb_base_key_cache'])!r}, '{def_name}_cache', cache_key)
  1856. loaded_values = values['__qweb_loaded_values']
  1857. def {def_name}_cache():
  1858. content = []
  1859. text = []
  1860. for item in {def_name}(self, {{**values, '__qweb_in_cache': True}}):
  1861. if isinstance(item, str):
  1862. text.append(item)
  1863. else:
  1864. content.append(''.join(text))
  1865. content.append(item)
  1866. text = []
  1867. if text:
  1868. content.append(''.join(text))
  1869. return content
  1870. cache_content = self._load_values(uniq_cache_key, {def_name}_cache, loaded_values)
  1871. if values.get('__qweb_in_cache'):
  1872. yield from cache_content
  1873. else:
  1874. for item in cache_content:
  1875. if isinstance(item, str):
  1876. yield item
  1877. else:
  1878. ref, function_name, cached_values = item
  1879. t_nocache_function = loaded_values.get(function_name)
  1880. if not t_nocache_function:
  1881. t_call_template_functions, def_name = self._compile(ref)
  1882. t_nocache_function = t_call_template_functions[function_name]
  1883. nocache_values = values['__qweb_root_values'].copy()
  1884. nocache_values.update(cached_values)
  1885. yield ''.join(t_nocache_function(self, nocache_values))
  1886. """, level))
  1887. return code
  1888. def _compile_directive_nocache(self, el, compile_context, level):
  1889. """
  1890. The `t-nocache` directive makes it possible to force rendering
  1891. of a part even if it is in a `t-cache`. The values available in
  1892. the `t-nocache` are the one provided when calling the template
  1893. (and therefore ignores any t-set that could have been done).
  1894. The `t-nocache-*` are the values whose result of the
  1895. expression will be cached and added to the root's values when
  1896. rendering the no cache part. Only primitive types can be cached.
  1897. see: `t-cache`
  1898. """
  1899. if 't-nocache' not in el.attrib:
  1900. raise SyntaxError("t-nocache-* must be on the same node as t-nocache")
  1901. el.attrib.pop('t-nocache')
  1902. code = self._flush_text(compile_context, level)
  1903. # t-nocache-* will generate the values to put in cache
  1904. # must cosume this attributes before generate the cached content.
  1905. code_cache_values = []
  1906. for key in list(el.attrib):
  1907. if key.startswith('t-nocache-'):
  1908. expr = el.attrib.pop(key)
  1909. varname = key[10:]
  1910. if not VARNAME_REGEXP.match(varname):
  1911. raise ValueError(f'The varname {varname!r} can only contain alphanumeric characters and underscores.')
  1912. code_cache_values.append(indent_code(f"""
  1913. cached_value = {self._compile_expr(expr)}
  1914. if cached_value is not None and not isinstance(cached_value, (str, int, float, bool)):
  1915. raise ValueError(f'''The value type of {key!r} cannot be cached: {{cached_value!r}}''')
  1916. cached_values[{varname!r}] = cached_value
  1917. """, level + 1))
  1918. # generate the cached content method
  1919. def_name = compile_context['make_name']('t_nocache')
  1920. def_code = [f"def {def_name}(self, values):"]
  1921. def_code.append(indent_code("try:", 1))
  1922. def_content = self._compile_directives(el, compile_context, 2)
  1923. if def_content and not compile_context['_text_concat']:
  1924. self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
  1925. def_code.extend(def_content)
  1926. def_code.extend(self._flush_text(compile_context, 2))
  1927. def_code.append(indent_code(f"""
  1928. except QWebException:
  1929. raise
  1930. except Exception as e:
  1931. raise QWebException("Error while render the template",
  1932. self, template, ref={compile_context['ref']!r}, code=code) from e
  1933. """, 1))
  1934. compile_context['template_functions'][def_name] = def_code
  1935. # if the nocache is inside a cache return a tuple with the method name and the cached values
  1936. code.append(indent_code("""
  1937. if values.get('__qweb_in_cache'):
  1938. cached_values = {}
  1939. """, level))
  1940. code.extend(code_cache_values)
  1941. code.append(indent_code(f"yield ({compile_context['template']!r}, {def_name!r}, cached_values)", level+1))
  1942. # else render the content
  1943. code.append(indent_code(f"""
  1944. else:
  1945. yield from {def_name}(self, values)
  1946. """, level))
  1947. return code
  1948. # methods called by the compiled function at rendering time.
  1949. def _debug_trace(self, debugger, values):
  1950. """Method called at running time to load debugger."""
  1951. if debugger in SUPPORTED_DEBUGGER:
  1952. __import__(debugger).set_trace()
  1953. else:
  1954. raise ValueError(f"unsupported t-debug value: {debugger}")
  1955. def _post_processing_att(self, tagName, atts):
  1956. """ Method called at compile time for the static node and called at
  1957. runing time for the dynamic attributes.
  1958. This method may be overwrited to filter or modify the attributes
  1959. (during compilation for static node or after they compilation in
  1960. the case of dynamic elements).
  1961. @returns dict
  1962. """
  1963. return atts
  1964. def _get_field(self, record, field_name, expression, tagName, field_options, values):
  1965. """Method called at compile time to return the field value.
  1966. :returns: tuple:
  1967. * dict: attributes
  1968. * string or None: content
  1969. * boolean: force_display display the tag if the content and default_content are None
  1970. """
  1971. field = record._fields[field_name]
  1972. # adds generic field options
  1973. field_options['tagName'] = tagName
  1974. field_options['expression'] = expression
  1975. field_options['type'] = field_options.get('widget', field.type)
  1976. inherit_branding = (
  1977. self.env.context['inherit_branding']
  1978. if 'inherit_branding' in self.env.context
  1979. else self.env.context.get('inherit_branding_auto') and record.check_access_rights('write', False))
  1980. field_options['inherit_branding'] = inherit_branding
  1981. translate = self.env.context.get('edit_translations') and values.get('translatable') and field.translate
  1982. field_options['translate'] = translate
  1983. # field converter
  1984. model = 'ir.qweb.field.' + field_options['type']
  1985. converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
  1986. # get content (the return values from fields are considered to be markup safe)
  1987. content = converter.record_to_html(record, field_name, field_options)
  1988. attributes = converter.attributes(record, field_name, field_options, values)
  1989. return (attributes, content, inherit_branding or translate)
  1990. def _get_widget(self, value, expression, tagName, field_options, values):
  1991. """Method called at compile time to return the widget value.
  1992. :returns: tuple:
  1993. * dict: attributes
  1994. * string or None: content
  1995. * boolean: force_display display the tag if the content and default_content are None
  1996. """
  1997. field_options['type'] = field_options['widget']
  1998. field_options['tagName'] = tagName
  1999. field_options['expression'] = expression
  2000. inherit_branding = self.env.context.get('inherit_branding')
  2001. field_options['inherit_branding'] = inherit_branding
  2002. # field converter
  2003. model = 'ir.qweb.field.' + field_options['type']
  2004. converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
  2005. # get content (the return values from widget are considered to be markup safe)
  2006. content = converter.value_to_html(value, field_options)
  2007. attributes = {}
  2008. attributes['data-oe-type'] = field_options['type']
  2009. attributes['data-oe-expression'] = field_options['expression']
  2010. return (attributes, content, inherit_branding)
  2011. def _get_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
  2012. """Generates asset nodes.
  2013. If debug=assets, the assets will be regenerated when a file which composes them has been modified.
  2014. Else, the assets will be generated only once and then stored in cache.
  2015. """
  2016. if debug and 'assets' in debug:
  2017. return self._generate_asset_nodes(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
  2018. else:
  2019. return self._generate_asset_nodes_cache(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
  2020. # qweb cache feature
  2021. def _get_cache_key(self, cache_key):
  2022. """
  2023. Convert the template cache key item into a hashable key.
  2024. :param cache_key: tuple
  2025. :returns: tuple of hashable items
  2026. """
  2027. if not isinstance(cache_key, (tuple, list)):
  2028. cache_key = (cache_key,)
  2029. keys = []
  2030. for item in cache_key:
  2031. try:
  2032. # use try catch instead of isinstance to detect lazy values
  2033. keys.append(item._name)
  2034. keys.append(tuple(item.ids))
  2035. dates = item.mapped('write_date')
  2036. if dates:
  2037. keys.append(max(dates).timestamp())
  2038. except AttributeError:
  2039. keys.append(repr(item))
  2040. return tuple(keys)
  2041. def _load_values(self, cache_key, get_value, loaded_values=None):
  2042. """ generate value from the function if the result is not cached. """
  2043. if not cache_key:
  2044. return get_value()
  2045. value = loaded_values and loaded_values.get(cache_key)
  2046. if not value:
  2047. value = self._get_cached_values(cache_key, get_value)
  2048. if loaded_values is not None:
  2049. loaded_values[cache_key] = value
  2050. return value
  2051. # The cache does not need to be invalidated if the 'base_key_cache'
  2052. # in '_compile' method contains the write_date of all inherited views.
  2053. @tools.conditional(
  2054. 'xml' not in tools.config['dev_mode'],
  2055. tools.ormcache('cache_key'),
  2056. )
  2057. def _get_cached_values(self, cache_key, get_value):
  2058. """ generate value from the function if the result is not cached. """
  2059. return get_value()
  2060. # other methods used for the asset bundles
  2061. @tools.conditional(
  2062. # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
  2063. # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
  2064. 'xml' not in tools.config['dev_mode'],
  2065. tools.ormcache('bundle', 'css', 'js', 'debug', 'async_load', 'defer_load', 'lazy_load', 'media', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
  2066. )
  2067. def _generate_asset_nodes_cache(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
  2068. return self._generate_asset_nodes(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
  2069. @tools.ormcache('bundle', 'defer_load', 'lazy_load', 'media', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())')
  2070. def _get_asset_content(self, bundle, defer_load=False, lazy_load=False, media=None):
  2071. asset_paths = self.env['ir.asset']._get_asset_paths(bundle=bundle, css=True, js=True)
  2072. files = []
  2073. remains = []
  2074. for path, *_ in asset_paths:
  2075. ext = path.split('.')[-1]
  2076. is_js = ext in SCRIPT_EXTENSIONS
  2077. is_xml = ext in TEMPLATE_EXTENSIONS
  2078. is_css = ext in STYLE_EXTENSIONS
  2079. if not is_js and not is_xml and not is_css:
  2080. continue
  2081. if is_xml:
  2082. base = get_module_path(bundle.split('.')[0]).rsplit('/', 1)[0]
  2083. if path.startswith(base):
  2084. path = path[len(base):]
  2085. mimetype = None
  2086. if is_js:
  2087. mimetype = 'text/javascript'
  2088. elif is_css:
  2089. mimetype = f'text/{ext}'
  2090. elif is_xml:
  2091. mimetype = 'text/xml'
  2092. if can_aggregate(path):
  2093. segments = [segment for segment in path.split('/') if segment]
  2094. files.append({
  2095. 'atype': mimetype,
  2096. 'url': path,
  2097. 'filename': get_resource_path(*segments) if segments else None,
  2098. 'content': '',
  2099. 'media': media,
  2100. })
  2101. else:
  2102. if is_js:
  2103. tag = 'script'
  2104. attributes = {
  2105. "type": mimetype,
  2106. }
  2107. attributes["data-src" if lazy_load else "src"] = path
  2108. if defer_load or lazy_load:
  2109. attributes["defer"] = "defer"
  2110. elif is_css:
  2111. tag = 'link'
  2112. attributes = {
  2113. "type": mimetype,
  2114. "rel": "stylesheet",
  2115. "href": path,
  2116. 'media': media,
  2117. }
  2118. elif is_xml:
  2119. tag = 'script'
  2120. attributes = {
  2121. "type": mimetype,
  2122. "async": "async",
  2123. "rel": "prefetch",
  2124. "data-src": path,
  2125. }
  2126. remains.append((tag, attributes, None))
  2127. return (files, remains)
  2128. def _get_asset_bundle(self, bundle_name, files, env=None, css=True, js=True):
  2129. return AssetsBundle(bundle_name, files, env=env, css=css, js=js)
  2130. def _generate_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
  2131. files, remains = self._get_asset_content(bundle, defer_load=defer_load, lazy_load=lazy_load, media=css and media or None)
  2132. asset = self._get_asset_bundle(bundle, files, env=self.env, css=css, js=js)
  2133. remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] == 'script')]
  2134. return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load, defer_load=defer_load, lazy_load=lazy_load)
  2135. def _get_asset_link_urls(self, bundle, debug=False):
  2136. asset_nodes = self._get_asset_nodes(bundle, js=False, debug=debug)
  2137. return [node[1]['href'] for node in asset_nodes if node[0] == 'link']
  2138. def _pregenerate_assets_bundles(self):
  2139. """
  2140. Pregenerates all assets that may be used in web pages to speedup first loading.
  2141. This may is mainly usefull for tests.
  2142. The current version is looking for all t-call-assets in view to generate the minimal
  2143. set of bundles to generate.
  2144. Current version only generate assets without extra, not taking care of rtl.
  2145. """
  2146. _logger.runbot('Pregenerating assets bundles')
  2147. views = self.env['ir.ui.view'].search([('type', '=', 'qweb'), ('arch_db', 'like', 't-call-assets')])
  2148. js_bundles = set()
  2149. css_bundles = set()
  2150. for view in views:
  2151. for call_asset in etree.fromstring(view.arch_db).xpath("//*[@t-call-assets]"):
  2152. asset = call_asset.get('t-call-assets')
  2153. js = str2bool(call_asset.get('t-js', 'True'))
  2154. css = str2bool(call_asset.get('t-css', 'True'))
  2155. if js:
  2156. js_bundles.add(asset)
  2157. if css:
  2158. css_bundles.add(asset)
  2159. nodes = []
  2160. start = time.time()
  2161. for bundle in sorted(js_bundles):
  2162. nodes += self._generate_asset_nodes(bundle, css=False, js=True)
  2163. _logger.info('JS Assets bundles generated in %s seconds', time.time()-start)
  2164. start = time.time()
  2165. for bundle in sorted(css_bundles):
  2166. nodes += self._generate_asset_nodes(bundle, css=True, js=False)
  2167. _logger.info('CSS Assets bundles generated in %s seconds', time.time()-start)
  2168. return nodes
  2169. def render(template_name, values, load, **options):
  2170. """ Rendering of a qweb template without database and outside the registry.
  2171. (Widget, field, or asset rendering is not implemented.)
  2172. :param (string|int) template_name: template identifier
  2173. :param dict values: template values to be used for rendering
  2174. :param def load: function like `load(template_name)` which returns an etree
  2175. from the given template name (from initial rendering or template
  2176. `t-call`).
  2177. :param options: used to compile the template
  2178. :returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
  2179. instead of `str`)
  2180. :rtype: MarkupSafe
  2181. """
  2182. class MockPool:
  2183. db_name = None
  2184. _Registry__cache = {}
  2185. class MockIrQWeb(IrQWeb):
  2186. _register = False # not visible in real registry
  2187. pool = MockPool()
  2188. def _load(self, ref):
  2189. """
  2190. Load the template referenced by ``ref``.
  2191. :returns: The loaded template (as string or etree) and its
  2192. identifier
  2193. :rtype: Tuple[Union[etree, str], Optional[str, int]]
  2194. """
  2195. return self.env.context['load'](ref)
  2196. def _prepare_environment(self, values):
  2197. values['true'] = True
  2198. values['false'] = False
  2199. return self.with_context(is_t_cache_disabled=True, __qweb_loaded_values={})
  2200. def _get_field(self, *args):
  2201. raise NotImplementedError("Fields are not allowed in this rendering mode. Please use \"env['ir.qweb']._render\" method")
  2202. def _get_widget(self, *args):
  2203. raise NotImplementedError("Widgets are not allowed in this rendering mode. Please use \"env['ir.qweb']._render\" method")
  2204. def _get_asset_nodes(self, *args):
  2205. raise NotImplementedError("Assets are not allowed in this rendering mode. Please use \"env['ir.qweb']._render\" method")
  2206. class MockEnv(dict):
  2207. def __init__(self):
  2208. super().__init__()
  2209. self.context = {}
  2210. def __call__(self, cr=None, user=None, context=None, su=None):
  2211. """ Return an mocked environment based and update the sent context.
  2212. Allow to use `ir_qweb.with_context` with sand boxed qweb.
  2213. """
  2214. env = MockEnv()
  2215. env.context.update(self.context if context is None else context)
  2216. return env
  2217. renderer = MockIrQWeb(MockEnv(), tuple(), tuple())
  2218. return renderer._render(template_name, values, load=load, minimal_qcontext=True, **options)