summaryrefslogtreecommitdiff
path: root/addons/web
diff options
context:
space:
mode:
authorLucas Perais (lpe) <lpe@odoo.com>2019-06-04 13:29:58 +0000
committerVincent Schippefilt <vsc@odoo.com>2019-07-24 12:21:59 +0000
commitfc5878ecc668ee83cd1a374474f0b21bf6a51940 (patch)
treeb08e064a72f84d4fb14a0b5560ae1fd142c05563 /addons/web
parent8be4567ae8f6b0388206c091590278ef11a2df21 (diff)
[IMP] base, web: static xml templates support serverside inheritance
QWeb templates that show up in the 'qweb' key of a module's manifest now support server side inheritance and xpath evaluation QWeb templates that show up in the xmlDependencies of a JS widget are not impacted at all by theses changes, as they are served through the Werkzeug sharedMiddleware A similar syntax than ir.ui.view has been implemented in the QWeb templates - each template must have a root node, whatever tag works - the root node of a template must have a t-name containing the name of the template The name -- without the module's name -- may contain dots pretty much anywhere Though what is recommended is only underscores in template names - if a template is to inherit from a parent, the root node has a t-inherit directive containing either the full name of the template it inherits from which is module_name.template_name or the name of the template, no module name necessary, if the parent template is in the same module - there are 2 modes of inheriting primary: copy the behavior of the parent into the template extension: modifies the parent in place Task: 1999528 closes odoo/odoo#33892 Signed-off-by: VincentSchippefilt <VincentSchippefilt@users.noreply.github.com> Co-authored-by: Julien Mougenot <jum@odoo.com> Co-authored-by: Lucas Perais <lpe@odoo.com>
Diffstat (limited to 'addons/web')
-rw-r--r--addons/web/controllers/main.py241
-rw-r--r--addons/web/models/ir_http.py5
-rw-r--r--addons/web/static/tests/services/notification_service_tests.js12
-rw-r--r--addons/web/tests/test_serving_base.py417
4 files changed, 623 insertions, 52 deletions
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index d77a4ea9e7e..0a178431f2a 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -3,6 +3,7 @@
import babel.messages.pofile
import base64
+import copy
import datetime
import functools
import glob
@@ -19,16 +20,15 @@ import re
import sys
import tempfile
import time
-import zlib
import werkzeug
import werkzeug.exceptions
import werkzeug.utils
import werkzeug.wrappers
import werkzeug.wsgi
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
from werkzeug.urls import url_decode, iri_to_uri
-from xml.etree import ElementTree
+from lxml import etree
import unicodedata
@@ -36,7 +36,7 @@ import odoo
import odoo.modules.registry
from odoo.api import call_kw, Environment
from odoo.modules import get_module_path, get_resource_path
-from odoo.tools import image_process, topological_sort, html_escape, pycompat, ustr
+from odoo.tools import image_process, topological_sort, html_escape, pycompat, ustr, apply_inheritance_specs
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.translate import _
from odoo.tools.misc import str2bool, xlwt, file_open
@@ -67,6 +67,8 @@ CONTENT_MAXAGE = 60 * 60 * 24 * 356
DBNAME_PATTERN = '^[a-zA-Z0-9][a-zA-Z0-9_.-]+$'
+COMMENT_PATTERN = r'Modified by [\s\w\-.]+ from [\s\w\-.]+'
+
#----------------------------------------------------------
# Odoo Web helpers
#----------------------------------------------------------
@@ -195,37 +197,6 @@ def module_boot(db=None):
addons = serverside + dbside
return addons
-def concat_xml(file_list):
- """Concatenate xml files
-
- :param list(str) file_list: list of files to check
- :returns: (concatenation_result, checksum)
- :rtype: (str, str)
- """
- checksum = hashlib.new('sha1')
- if not file_list:
- return '', checksum.hexdigest()
-
- root = None
- for fname in file_list:
- with open(fname, 'rb') as fp:
- contents = fp.read()
- checksum.update(contents)
- fp.seek(0)
- try:
- xml = ElementTree.parse(fp).getroot()
- except ElementTree.ParseError as e:
- _logger.error("Could not parse file %s: %s" % (fname, e.msg))
- raise e
-
- if root is None:
- root = ElementTree.Element(xml.tag)
- #elif root.tag != xml.tag:
- # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
-
- for child in xml.getchildren():
- root.append(child)
- return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
def fs2web(path):
"""convert FS path into web path"""
@@ -234,8 +205,7 @@ def fs2web(path):
def manifest_glob(extension, addons=None, db=None, include_remotes=False):
if addons is None:
addons = module_boot(db=db)
- else:
- addons = addons.split(',')
+
r = []
for addon in addons:
manifest = http.addons_manifest.get(addon, None)
@@ -247,12 +217,13 @@ def manifest_glob(extension, addons=None, db=None, include_remotes=False):
for pattern in globlist:
if pattern.startswith(('http://', 'https://', '//')):
if include_remotes:
- r.append((None, pattern))
+ r.append((None, pattern, addon))
else:
for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
- r.append((path, fs2web(path[len(addons_path):])))
+ r.append((path, fs2web(path[len(addons_path):]), addon))
return r
+
def manifest_list(extension, mods=None, db=None, debug=None):
""" list resources to load specifying either:
mods: a comma separated string listing modules
@@ -260,8 +231,9 @@ def manifest_list(extension, mods=None, db=None, debug=None):
"""
if debug is not None:
_logger.warning("odoo.addons.web.main.manifest_list(): debug parameter is deprecated")
+ mods = mods.split(',')
files = manifest_glob(extension, addons=mods, db=db, include_remotes=True)
- return [wp for _fp, wp in files]
+ return [wp for _fp, wp, addon in files]
def get_last_modified(files):
""" Returns the modification time of the most recently modified
@@ -450,6 +422,191 @@ def _admin_password_warn(uid):
subtype='mail.mt_comment'
)
+
+class HomeStaticTemplateHelpers(object):
+ """
+ Helper Class that wraps the reading of static qweb templates files
+ and xpath inheritance applied to those templates
+ /!\ Template inheritance order is defined by ir.module.module natural order
+ which is "sequence, name"
+ Then a topological sort is applied, which just puts dependencies
+ of a module before that module
+ """
+ NAME_TEMPLATE_DIRECTIVE = 't-name'
+ STATIC_INHERIT_DIRECTIVE = 't-inherit'
+ STATIC_INHERIT_MODE_DIRECTIVE = 't-inherit-mode'
+ PRIMARY_MODE = 'primary'
+ EXTENSION_MODE = 'extension'
+ DEFAULT_MODE = PRIMARY_MODE
+
+ def __init__(self, addons, db, checksum_only=False, debug=False):
+ '''
+ :param str|list addons: plain list or comma separated list of addons
+ :param str db: the current db we are working on
+ :param bool checksum_only: only computes the checksum of all files for addons
+ :param str debug: the debug mode of the session
+ '''
+ super(HomeStaticTemplateHelpers, self).__init__()
+ self.addons = addons.split(',') if isinstance(addons, str) else addons
+ self.db = db
+ self.debug = debug
+ self.checksum_only = checksum_only
+ self.template_dict = OrderedDict()
+
+ def _get_parent_template(self, addon, template):
+ """Computes the real addon name and the template name
+ of the parent template (the one that is inherited from)
+
+ :param str addon: the addon the template is declared in
+ :param etree template: the current template we are are handling
+ :returns: (str, str)
+ """
+ original_template_name = template.attrib[self.STATIC_INHERIT_DIRECTIVE]
+ split_name_attempt = original_template_name.split('.', 1)
+ parent_addon, parent_name = tuple(split_name_attempt) if len(split_name_attempt) == 2 else (addon, original_template_name)
+ if parent_addon not in self.template_dict:
+ if original_template_name in self.template_dict[addon]:
+ parent_addon = addon
+ parent_name = original_template_name
+ else:
+ raise ValueError(_('Module %s not loaded or inexistent, or templates of addon being loaded (%s) are misordered') % (parent_addon, addon))
+
+ if parent_name not in self.template_dict[parent_addon]:
+ raise ValueError(_("No template found to inherit from. Module %s and template name %s") % (parent_addon, parent_name))
+
+ return parent_addon, parent_name
+
+ def _compute_xml_tree(self, addon, file_name, source):
+ """Computes the xml tree that 'source' contains
+ Applies inheritance specs in the process
+
+ :param str addon: the current addon we are reading files for
+ :param str file_name: the current name of the file we are reading
+ :param str source: the content of the file
+ :returns: etree
+ """
+ try:
+ all_templates_tree = etree.parse(io.BytesIO(source), parser=etree.XMLParser(remove_comments=True)).getroot()
+ except etree.ParseError as e:
+ _logger.error("Could not parse file %s: %s" % (file_name, e.msg))
+ raise e
+
+ self.template_dict.setdefault(addon, OrderedDict())
+ for template_tree in list(all_templates_tree):
+ if self.NAME_TEMPLATE_DIRECTIVE in template_tree.attrib:
+ template_name = template_tree.attrib[self.NAME_TEMPLATE_DIRECTIVE]
+ else:
+ # self.template_dict[addon] grows after processing each template
+ template_name = 'anonymous_template_%s' % len(self.template_dict[addon])
+ if self.STATIC_INHERIT_DIRECTIVE in template_tree.attrib:
+ inherit_mode = template_tree.attrib.get(self.STATIC_INHERIT_MODE_DIRECTIVE, self.DEFAULT_MODE)
+ if inherit_mode not in [self.PRIMARY_MODE, self.EXTENSION_MODE]:
+ raise ValueError(_("Invalid inherit mode. Module %s and template name %s") % (addon, template_name))
+
+ parent_addon, parent_name = self._get_parent_template(addon, template_tree)
+
+ # After several performance tests, we found out that deepcopy is the most efficient
+ # solution in this case (compared with copy, xpath with '.' and stringifying).
+ parent_tree = copy.deepcopy(self.template_dict[parent_addon][parent_name])
+ parent_tag = parent_tree.tag
+ # replace temporarily the parent tag so it is never the target of the inheritance
+ parent_tree.tag = 't'
+ xpaths = list(template_tree)
+ if self.debug and inherit_mode == self.EXTENSION_MODE:
+ for xpath in xpaths:
+ xpath.insert(0, etree.Comment(" Modified by %s from %s " % (template_name, addon)))
+ inherited_template = apply_inheritance_specs(parent_tree, xpaths)
+ inherited_template.tag = parent_tag
+
+ if inherit_mode == self.PRIMARY_MODE: # New template_tree: A' = B(A)
+ inherited_template.set(self.NAME_TEMPLATE_DIRECTIVE, template_name)
+ inherited_template.set(self.STATIC_INHERIT_DIRECTIVE, template_tree.attrib[self.STATIC_INHERIT_DIRECTIVE])
+ if self.debug:
+ self._remove_inheritance_comments(inherited_template)
+ self.template_dict[addon][template_name] = inherited_template
+
+ else: # Modifies original: A = B(A)
+ self.template_dict[parent_addon][parent_name] = inherited_template
+ else:
+ if template_name in self.template_dict[addon]:
+ raise ValueError(_("Template %s already exists in module %s") % (template_name, addon))
+ self.template_dict[addon][template_name] = template_tree
+ return all_templates_tree
+
+ def _remove_inheritance_comments(self, inherited_template):
+ '''Remove the comments added in the template already, they come from other templates extending
+ the base of this inheritance
+
+ :param inherited_template:
+ '''
+ for comment in inherited_template.xpath('//comment()'):
+ if re.match(COMMENT_PATTERN, comment.text.strip()):
+ comment.getparent().remove(comment)
+
+ def _manifest_glob(self):
+ '''Proxy for manifest_glob
+ Usefull to make 'self' testable'''
+ return manifest_glob('qweb', self.addons, self.db)
+
+ def _read_addon_file(self, file_path):
+ """Reads the content of a file given by file_path
+ Usefull to make 'self' testable
+ :param str file_path:
+ :returns: str
+ """
+ with open(file_path, 'rb') as fp:
+ contents = fp.read()
+ return contents
+
+ def _concat_xml(self, file_dict):
+ """Concatenate xml files
+
+ :param dict(list) file_dict:
+ key: addon name
+ value: list of files for an addon
+ :returns: (concatenation_result, checksum)
+ :rtype: (str, str)
+ """
+ checksum = hashlib.new('sha1')
+ if not file_dict:
+ return '', checksum.hexdigest()
+
+ root = None
+ for addon, fnames in file_dict.items():
+ for fname in fnames:
+ contents = self._read_addon_file(fname)
+ checksum.update(contents)
+ if not self.checksum_only:
+ xml = self._compute_xml_tree(addon, fname, contents)
+
+ if root is None:
+ root = etree.Element(xml.tag)
+
+ for addon in self.template_dict.values():
+ for template in addon.values():
+ root.append(template)
+
+ return etree.tostring(root, encoding='utf-8') if root is not None else '', checksum.hexdigest()
+
+ def _get_qweb_templates(self):
+ """One and only entry point that gets and evaluates static qweb templates
+
+ :rtype: (str, str)
+ """
+ files = OrderedDict([(addon, list()) for addon in self.addons])
+ [files[f[2]].append(f[0]) for f in self._manifest_glob()]
+ content, checksum = self._concat_xml(files)
+ return content, checksum
+
+ @classmethod
+ def get_qweb_templates_checksum(cls, addons, db=None, debug=False):
+ return cls(addons, db, checksum_only=True, debug=debug)._get_qweb_templates()[1]
+
+ @classmethod
+ def get_qweb_templates(cls, addons, db=None, debug=False):
+ return cls(addons, db, debug=debug)._get_qweb_templates()[0]
+
+
#----------------------------------------------------------
# Odoo Web web Controllers
#----------------------------------------------------------
@@ -589,8 +746,8 @@ class WebClient(http.Controller):
@http.route('/web/webclient/qweb/<string:unique>', type='http', auth="none", cors="*")
def qweb(self, unique, mods=None, db=None):
- files = [f[0] for f in manifest_glob('qweb', addons=mods, db=db)]
- content, _dummy = concat_xml(files)
+ content = HomeStaticTemplateHelpers.get_qweb_templates(mods, db, debug=request.session.debug)
+
return request.make_response(content, [
('Content-Type', 'text/xml'),
('Cache-Control','public, max-age=' + str(CONTENT_MAXAGE))
diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py
index 24b2ec551f7..5d1ac1aac11 100644
--- a/addons/web/models/ir_http.py
+++ b/addons/web/models/ir_http.py
@@ -7,7 +7,7 @@ from odoo import api, models
from odoo.http import request
from odoo.tools import ustr
-from odoo.addons.web.controllers.main import concat_xml, manifest_glob, module_boot
+from odoo.addons.web.controllers.main import module_boot, HomeStaticTemplateHelpers
import odoo
@@ -28,8 +28,7 @@ class Http(models.AbstractModel):
user_context = request.session.get_context() if request.session.uid else {}
mods = module_boot()
- files = [f[0] for f in manifest_glob('qweb', addons=','.join(mods))]
- _, qweb_checksum = concat_xml(files)
+ qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(addons=mods, debug=request.session.debug)
lang = user_context.get("lang")
translations_per_module, lang_params = request.env['ir.translation'].get_translations_for_webclient(mods, lang)
diff --git a/addons/web/static/tests/services/notification_service_tests.js b/addons/web/static/tests/services/notification_service_tests.js
index 303b38bbe3b..01fa2a9f8b0 100644
--- a/addons/web/static/tests/services/notification_service_tests.js
+++ b/addons/web/static/tests/services/notification_service_tests.js
@@ -51,8 +51,8 @@ QUnit.module('Services', {
});
await testUtils.nextMicrotaskTick();
var $notification = $('body .o_notification_manager .o_notification');
- assert.strictEqual(_.str.trim($notification.html().replace(/\s+/g, ' ')),
- "<div class=\"toast-header\"> <span role=\"img\" aria-label=\"Notification undefined\" class=\"fa fa-2x mr-3 fa-lightbulb-o o_notification_icon\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a</div> </div> <div class=\"toast-body\"> <div class=\"o_notification_content\">b</div> </div>",
+ assert.strictEqual($notification.html().trim().replace(/\s+/g, ' '),
+ "<div class=\"toast-header\"> <span class=\"fa fa-2x mr-3 fa-lightbulb-o o_notification_icon\" role=\"img\" aria-label=\"Notification undefined\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a</div> </div> <div class=\"toast-body\"> <div class=\"o_notification_content\">b</div> </div>",
"should display notification");
assert.containsNone($notification, '.o_notification_close', "should not display the close button in ");
await testUtils.nextTick();
@@ -72,8 +72,8 @@ QUnit.module('Services', {
});
await testUtils.nextMicrotaskTick();
var $notification = $('body .o_notification_manager .o_notification');
- assert.strictEqual(_.str.trim($notification.html().replace(/\s+/g, ' ')),
- "<div class=\"toast-header\"> <span role=\"img\" aria-label=\"Notification undefined\" class=\"fa fa-2x mr-3 fa-exclamation o_notification_icon\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a</div> </div> <div class=\"toast-body\"> <div class=\"o_notification_content\">b</div> </div>",
+ assert.strictEqual($notification.html().trim().replace(/\s+/g, ' '),
+ "<div class=\"toast-header\"> <span class=\"fa fa-2x mr-3 fa-exclamation o_notification_icon\" role=\"img\" aria-label=\"Notification undefined\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a</div> </div> <div class=\"toast-body\"> <div class=\"o_notification_content\">b</div> </div>",
"should display notification");
view.destroy();
});
@@ -180,8 +180,8 @@ QUnit.module('Services', {
var $notification = $('body .o_notification_manager .o_notification');
assert.containsOnce($notification.eq(0), '.o_notification_close',
"should display the close button in notification");
- assert.strictEqual(_.str.trim($notification.eq(0).html().replace(/\s+/g, ' ')),
- "<div class=\"toast-header\"> <span role=\"img\" aria-label=\"Notification undefined\" class=\"fa fa-2x mr-3 fa-question-circle-o o_notification_icon\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a0</div> <button aria-label=\"Close\" class=\"mb-1 close o_notification_close\" data-dismiss=\"toast\" type=\"button\"> <span aria-hidden=\"true\" class=\"d-inline\">×</span> </button> </div> <div class=\"toast-body\"> <div class=\"o_notification_content\">b0</div> <div class=\"mt-2 o_notification_buttons\"> <button class=\"btn btn-primary\" type=\"button\"> <span>accept0</span> </button><button class=\"btn btn-secondary\" type=\"button\"> <span>refuse0</span> </button> </div> </div>",
+ assert.strictEqual($notification.html().trim().replace(/\s+/g, ' '),
+ "<div class=\"toast-header\"> <span class=\"fa fa-2x mr-3 fa-question-circle-o o_notification_icon\" role=\"img\" aria-label=\"Notification undefined\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a0</div> <button type=\"button\" class=\"mb-1 close o_notification_close\" data-dismiss=\"toast\" aria-label=\"Close\"> <span class=\"d-inline\" aria-hidden=\"true\">×</span> </button> </div> <div class=\"toast-body\"> <div class=\"o_notification_content\">b0</div> <div class=\"mt-2 o_notification_buttons\"> <button type=\"button\" class=\"btn btn-primary\"> <span>accept0</span> </button><button type=\"button\" class=\"btn btn-secondary\"> <span>refuse0</span> </button> </div> </div>",
"should display notification");
testUtils.dom.click($notification.find('.o_notification_buttons button:contains(accept0)'));
diff --git a/addons/web/tests/test_serving_base.py b/addons/web/tests/test_serving_base.py
index e1c6c7d5ab0..bda394c5152 100644
--- a/addons/web/tests/test_serving_base.py
+++ b/addons/web/tests/test_serving_base.py
@@ -2,9 +2,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import random
+import re
+import unittest
+from unittest.mock import patch
+import textwrap
+from datetime import datetime, timedelta
+from lxml import etree
-from odoo.tests.common import BaseCase
+from odoo.tests.common import BaseCase, tagged
from odoo.tools import topological_sort
+from odoo.addons.web.controllers.main import HomeStaticTemplateHelpers
def sample(population):
@@ -36,3 +43,411 @@ class TestModulesLoading(BaseCase):
module, sorted_modules.index(module), deps, seen
))
seen.add(module)
+
+
+@tagged('standard', 'at_install', 'static_templates')
+class TestStaticInheritance(unittest.TestCase):
+
+ def setUp(self):
+ super(TestStaticInheritance, self).setUp()
+ # output is "manifest_glob" return
+ self.modules = [
+ ('module_1_file_1', None, 'module_1'),
+ ('module_2_file_1', None, 'module_2'),
+ ]
+
+ self.template_files = {
+ 'module_1_file_1': b"""
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_1_1" random-attr="gloria">
+ <div>At first I was afraid</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <t t-name="template_1_2">
+ <div>And I grew strong</div>
+ </t>
+ </templates>
+ """,
+
+ 'module_2_file_1': b"""
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_2_1" t-inherit="module_1.template_1_1" t-inherit-mode="primary">
+ <xpath expr="//div[1]" position="after">
+ <div>I was petrified</div>
+ </xpath>
+ <xpath expr="//div[2]" position="after">
+ <div>But then I spent so many nights thinking how you did me wrong</div>
+ </xpath>
+ </form>
+ <div t-name="template_2_2">
+ <div>And I learned how to get along</div>
+ </div>
+ <form t-inherit="module_1.template_1_2" t-inherit-mode="extension">
+ <xpath expr="//div[1]" position="after">
+ <div>And I learned how to get along</div>
+ </xpath>
+ </form>
+ </templates>
+ """,
+ }
+ self._set_patchers()
+ self._toggle_patchers('start')
+ self._reg_replace_ws = r"\s|\t"
+
+ def tearDown(self):
+ super(TestStaticInheritance, self).tearDown()
+ self._toggle_patchers('stop')
+
+ # Custom Assert
+ def assertXMLEqual(self, output, expected):
+ self.assertTrue(output)
+ self.assertTrue(expected)
+ output = textwrap.dedent(output.decode('UTF-8')).strip()
+ output = re.sub(self._reg_replace_ws, '', output)
+
+ expected = textwrap.dedent(expected.decode('UTF-8')).strip()
+ expected = re.sub(self._reg_replace_ws, '', expected)
+ self.assertEqual(output, expected)
+
+ # Private methods
+ def _get_module_names(self):
+ return ','.join([glob[2] for glob in self.modules])
+
+ def _set_patchers(self):
+ def _patched_for_manifest_glob(*args, **kwargs):
+ # Ordered by module
+ return self.modules
+
+ def _patch_for_read_addon_file(*args, **kwargs):
+ return self.template_files[args[1]]
+
+ self.patchers = [
+ patch.object(HomeStaticTemplateHelpers, '_manifest_glob', _patched_for_manifest_glob),
+ patch.object(HomeStaticTemplateHelpers, '_read_addon_file', _patch_for_read_addon_file),
+ ]
+
+ def _toggle_patchers(self, mode):
+ self.assertTrue(mode in ('start', 'stop'))
+ for p in self.patchers:
+ getattr(p, mode)()
+
+ # Actual test cases
+ def test_static_inheritance_01(self):
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="template_1_1" random-attr="gloria">
+ <div>At first I was afraid</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <t t-name="template_1_2">
+ <div>And I grew strong</div>
+ <!-- Modified by anonymous_template_2 from module_2 -->
+ <div>And I learned how to get along</div>
+ </t>
+ <form t-name="template_2_1" random-attr="gloria" t-inherit="module_1.template_1_1">
+ <div>At first I was afraid</div>
+ <div>I was petrified</div>
+ <div>But then I spent so many nights thinking how you did me wrong</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <div t-name="template_2_2">
+ <div>And I learned how to get along</div>
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_static_inheritance_in_same_module(self):
+ self.modules = [
+ ('module_1_file_1', None, 'module_1'),
+ ('module_1_file_2', None, 'module_1'),
+ ]
+
+ self.template_files = {
+ 'module_1_file_1': b'''
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_1_1">
+ <div>At first I was afraid</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ </templates>
+ ''',
+
+ 'module_1_file_2': b'''
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="primary">
+ <xpath expr="//div[1]" position="after">
+ <div>I was petrified</div>
+ </xpath>
+ </form>
+ </templates>
+ '''
+ }
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="template_1_1">
+ <div>At first I was afraid</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <form t-name="template_1_2" t-inherit="template_1_1">
+ <div>At first I was afraid</div>
+ <div>I was petrified</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_static_inheritance_in_same_file(self):
+ self.modules = [
+ ('module_1_file_1', None, 'module_1'),
+ ]
+
+ self.template_files = {
+ 'module_1_file_1': b'''
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_1_1">
+ <div>At first I was afraid</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <form t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="primary">
+ <xpath expr="//div[1]" position="after">
+ <div>I was petrified</div>
+ </xpath>
+ </form>
+ </templates>
+ ''',
+ }
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="template_1_1">
+ <div>At first I was afraid</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <form t-name="template_1_2" t-inherit="template_1_1">
+ <div>At first I was afraid</div>
+ <div>I was petrified</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_static_inherit_extended_template(self):
+ self.modules = [
+ ('module_1_file_1', None, 'module_1'),
+ ]
+ self.template_files = {
+ 'module_1_file_1': b'''
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_1_1">
+ <div>At first I was afraid</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <form t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="extension">
+ <xpath expr="//div[1]" position="after">
+ <div>I was petrified</div>
+ </xpath>
+ </form>
+ <form t-name="template_1_3" t-inherit="template_1_1" t-inherit-mode="primary">
+ <xpath expr="//div[3]" position="after">
+ <div>But then I spent so many nights thinking how you did me wrong</div>
+ </xpath>
+ </form>
+ </templates>
+ ''',
+ }
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="template_1_1">
+ <div>At first I was afraid</div>
+ <!-- Modified by template_1_2 from module_1 -->
+ <div>I was petrified</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ </form>
+ <form t-name="template_1_3" t-inherit="template_1_1">
+ <div>At first I was afraid</div>
+ <div>I was petrified</div>
+ <div>Kept thinking I could never live without you by my side</div>
+ <div>But then I spent so many nights thinking how you did me wrong</div>
+ </form>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_sibling_extension(self):
+ self.modules = [
+ ('module_1_file_1', None, 'module_1'),
+ ('module_2_file_1', None, 'module_2'),
+ ('module_3_file_1', None, 'module_3'),
+ ]
+ self.template_files = {
+ 'module_1_file_1': b'''
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_1_1">
+ <div>I am a man of constant sorrow</div>
+ <div>I've seen trouble all my days</div>
+ </form>
+ </templates>
+ ''',
+
+ 'module_2_file_1': b'''
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_2_1" t-inherit="module_1.template_1_1" t-inherit-mode="extension">
+ <xpath expr="//div[1]" position="after">
+ <div>In constant sorrow all through his days</div>
+ </xpath>
+ </form>
+ </templates>
+ ''',
+
+ 'module_3_file_1': b'''
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_3_1" t-inherit="module_1.template_1_1" t-inherit-mode="extension">
+ <xpath expr="//div[2]" position="after">
+ <div>Oh Brother !</div>
+ </xpath>
+ </form>
+ </templates>
+ '''
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="template_1_1">
+ <div>I am a man of constant sorrow</div>
+ <!-- Modified by template_2_1 from module_2 -->
+ <div>In constant sorrow all through his days</div>
+ <!-- Modified by template_3_1 from module_3 -->
+ <div>Oh Brother !</div>
+ <div>I've seen trouble all my days</div>
+ </form>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_static_misordered_modules(self):
+ self.modules.reverse()
+ with self.assertRaises(ValueError) as ve:
+ HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+
+ self.assertEqual(
+ str(ve.exception),
+ 'Module module_1 not loaded or inexistent, or templates of addon being loaded (module_2) are misordered'
+ )
+
+ def test_static_misordered_templates(self):
+ self.template_files['module_2_file_1'] = b"""
+ <templates id="template" xml:space="preserve">
+ <form t-name="template_2_1" t-inherit="module_2.template_2_2" t-inherit-mode="primary">
+ <xpath expr="//div[1]" position="after">
+ <div>I was petrified</div>
+ </xpath>
+ </form>
+ <div t-name="template_2_2">
+ <div>And I learned how to get along</div>
+ </div>
+ </templates>
+ """
+ with self.assertRaises(ValueError) as ve:
+ HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+
+ self.assertEqual(
+ str(ve.exception),
+ 'No template found to inherit from. Module module_2 and template name template_2_2'
+ )
+
+ def _sick_script(self, nMod, nFilePerMod, nTemplatePerFile, stepInheritInModule=2, stepInheritPreviousModule=3):
+ """
+ Make a sick amount of templates to test perf
+ nMod modules
+ each module: has nFilesPerModule files, each of which contains nTemplatePerFile templates
+ """
+ self.modules = []
+ self.template_files = {}
+ number_templates = 0
+ for m in range(nMod):
+ for f in range(nFilePerMod):
+ mname = 'mod_%s' % m
+ fname = 'mod_%s_file_%s' % (m, f)
+ self.modules.append((fname, None, mname))
+
+ _file = '<templates id="template" xml:space="preserve">'
+
+ for t in range(nTemplatePerFile):
+ _template = ''
+ if t % stepInheritInModule or t % stepInheritPreviousModule or t == 0:
+ _template += """
+ <div t-name="template_%(t_number)s_mod_%(m_number)s">
+ <div>Parent</div>
+ </div>
+ """
+
+ elif not t % stepInheritInModule and t >= 1:
+ _template += """
+ <div t-name="template_%(t_number)s_mod_%(m_number)s"
+ t-inherit="template_%(t_inherit)s_mod_%(m_number)s"
+ t-inherit-mode="primary">
+ <xpath expr="//div[1]" position="before">
+ <div>Sick XPath</div>
+ </xpath>
+ </div>
+ """
+
+ elif not t % stepInheritPreviousModule and m >= 1:
+ _template += """
+ <div t-name="template_%(t_number)s_mod_%(m_number)s"
+ t-inherit="mod_%(m_module_inherit)s.template_%(t_module_inherit)s_mod_%(m_module_inherit)s"
+ t-inherit-mode="primary">
+ <xpath expr="//div[1]" position="inside">
+ <div>Mental XPath</div>
+ </xpath>
+ </div>
+ """
+ if _template:
+ number_templates += 1
+
+ _template_number = 1000 * f + t
+ _file += _template % {
+ 't_number': _template_number,
+ 'm_number': m,
+ 't_inherit': _template_number - 1,
+ 't_module_inherit': _template_number,
+ 'm_module_inherit': m - 1,
+ }
+ _file += '</templates>'
+
+ self.template_files[fname] = _file.encode()
+ self.assertEqual(number_templates, nMod * nFilePerMod * nTemplatePerFile)
+
+ def test_performance_2500(self):
+ nMod, nFilePerMod, nTemplatePerFile = 50, 5, 10
+ self._sick_script(nMod, nFilePerMod, nTemplatePerFile)
+
+ before = datetime.now()
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ after = datetime.now()
+ self.assertLessEqual(after - before, timedelta(milliseconds=130))
+
+ whole_tree = etree.fromstring(contents)
+ self.assertEqual(len(whole_tree), nMod * nFilePerMod * nTemplatePerFile)
+
+ def test_performance_25000(self):
+ nMod, nFilePerMod, nTemplatePerFile = 50, 5, 100
+ self._sick_script(nMod, nFilePerMod, nTemplatePerFile)
+
+ before = datetime.now()
+ HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ after = datetime.now()
+
+ self.assertLessEqual(after - before, timedelta(milliseconds=1000))