summaryrefslogtreecommitdiff
path: root/addons/web/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/tests')
-rw-r--r--addons/web/tests/__init__.py10
-rw-r--r--addons/web/tests/logo_ci.pngbin0 -> 2899 bytes
-rw-r--r--addons/web/tests/odoo.pngbin0 -> 5631 bytes
-rw-r--r--addons/web/tests/sweden.pngbin0 -> 1640 bytes
-rw-r--r--addons/web/tests/test_base_document_layout.py226
-rw-r--r--addons/web/tests/test_click_everywhere.py42
-rw-r--r--addons/web/tests/test_image.py60
-rw-r--r--addons/web/tests/test_js.py42
-rw-r--r--addons/web/tests/test_menu.py55
-rw-r--r--addons/web/tests/test_read_progress_bar.py144
-rw-r--r--addons/web/tests/test_serving_base.py1002
11 files changed, 1581 insertions, 0 deletions
diff --git a/addons/web/tests/__init__.py b/addons/web/tests/__init__.py
new file mode 100644
index 00000000..daac593d
--- /dev/null
+++ b/addons/web/tests/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_image
+from . import test_js
+from . import test_menu
+from . import test_serving_base
+from . import test_click_everywhere
+from . import test_base_document_layout
+from . import test_read_progress_bar
diff --git a/addons/web/tests/logo_ci.png b/addons/web/tests/logo_ci.png
new file mode 100644
index 00000000..1a1ae58e
--- /dev/null
+++ b/addons/web/tests/logo_ci.png
Binary files differ
diff --git a/addons/web/tests/odoo.png b/addons/web/tests/odoo.png
new file mode 100644
index 00000000..dd7c1e62
--- /dev/null
+++ b/addons/web/tests/odoo.png
Binary files differ
diff --git a/addons/web/tests/sweden.png b/addons/web/tests/sweden.png
new file mode 100644
index 00000000..be480002
--- /dev/null
+++ b/addons/web/tests/sweden.png
Binary files differ
diff --git a/addons/web/tests/test_base_document_layout.py b/addons/web/tests/test_base_document_layout.py
new file mode 100644
index 00000000..0f3f9e8b
--- /dev/null
+++ b/addons/web/tests/test_base_document_layout.py
@@ -0,0 +1,226 @@
+import os
+from PIL import Image
+from functools import partial
+
+from odoo.tests import TransactionCase, tagged, Form
+from odoo.tools import frozendict, image_to_base64, hex_to_rgb
+
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+_file_cache = {}
+
+
+class TestBaseDocumentLayoutHelpers(TransactionCase):
+ #
+ # Public
+ #
+ def setUp(self):
+ super(TestBaseDocumentLayoutHelpers, self).setUp()
+ self.color_fields = ['primary_color', 'secondary_color']
+ self.company = self.env.company
+ self.css_color_error = 0
+ self._set_templates_and_layouts()
+ self._set_images()
+
+ def assertColors(self, checked_obj, expected):
+ _expected_getter = expected.get if isinstance(expected, dict) else partial(getattr, expected)
+ for fname in self.color_fields:
+ color1 = getattr(checked_obj, fname)
+ color2 = _expected_getter(fname)
+ if self.css_color_error:
+ self._compare_colors_rgb(color1, color2)
+ else:
+ self.assertEqual(color1, color2)
+
+ #
+ # Private
+ #
+ def _compare_colors_rgb(self, color1, color2):
+ self.assertEqual(bool(color1), bool(color2))
+ if not color1:
+ return
+ color1 = hex_to_rgb(color1)
+ color2 = hex_to_rgb(color2)
+ self.assertEqual(len(color1), len(color2))
+ for i in range(len(color1)):
+ self.assertAlmostEqual(color1[i], color2[i], delta=self.css_color_error)
+
+ def _get_images_for_test(self):
+ return ['sweden.png', 'odoo.png']
+
+ def _set_images(self):
+ for fname in self._get_images_for_test():
+ fname_split = fname.split('.')
+ if not fname_split[0] in _file_cache:
+ with Image.open(os.path.join(dir_path, fname), 'r') as img:
+ base64_img = image_to_base64(img, 'PNG')
+ primary, secondary = self.env['base.document.layout'].create(
+ {})._parse_logo_colors(base64_img)
+ _img = frozendict({
+ 'img': base64_img,
+ 'colors': {
+ 'primary_color': primary,
+ 'secondary_color': secondary,
+ },
+ })
+ _file_cache[fname_split[0]] = _img
+ self.company_imgs = frozendict(_file_cache)
+
+ def _set_templates_and_layouts(self):
+ self.layout_template1 = self.env['ir.ui.view'].create({
+ 'name': 'layout_template1',
+ 'key': 'web.layout_template1',
+ 'type': 'qweb',
+ 'arch': '''<div></div>''',
+ })
+ self.env['ir.model.data'].create({
+ 'name': self.layout_template1.name,
+ 'model': 'ir.ui.view',
+ 'module': 'web',
+ 'res_id': self.layout_template1.id,
+ })
+ self.default_colors = {
+ 'primary_color': '#000000',
+ 'secondary_color': '#000000',
+ }
+ self.report_layout1 = self.env['report.layout'].create({
+ 'view_id': self.layout_template1.id,
+ 'name': 'report_%s' % self.layout_template1.name,
+ })
+ self.layout_template2 = self.env['ir.ui.view'].create({
+ 'name': 'layout_template2',
+ 'key': 'web.layout_template2',
+ 'type': 'qweb',
+ 'arch': '''<div></div>''',
+ })
+ self.env['ir.model.data'].create({
+ 'name': self.layout_template2.name,
+ 'model': 'ir.ui.view',
+ 'module': 'web',
+ 'res_id': self.layout_template2.id,
+ })
+ self.report_layout2 = self.env['report.layout'].create({
+ 'view_id': self.layout_template2.id,
+ 'name': 'report_%s' % self.layout_template2.name,
+ })
+
+
+@tagged('document_layout')
+class TestBaseDocumentLayout(TestBaseDocumentLayoutHelpers):
+ # Logo change Tests
+ def test_company_no_color_change_logo(self):
+ """When neither a logo nor the colors are set
+ The wizard displays the colors of the report layout
+ Changing logo means the colors on the wizard change too
+ Emptying the logo works and doesn't change the colors"""
+ self.company.write({
+ 'primary_color': False,
+ 'secondary_color': False,
+ 'logo': False,
+ 'external_report_layout_id': self.env.ref('web.layout_template1').id,
+ 'paperformat_id': self.env.ref('base.paperformat_us').id,
+ })
+ default_colors = self.default_colors
+ with Form(self.env['base.document.layout']) as doc_layout:
+ self.assertColors(doc_layout, default_colors)
+ self.assertEqual(doc_layout.company_id, self.company)
+ doc_layout.logo = self.company_imgs['sweden']['img']
+
+ self.assertColors(doc_layout, self.company_imgs['sweden']['colors'])
+
+ doc_layout.logo = ''
+ self.assertColors(doc_layout, self.company_imgs['sweden']['colors'])
+ self.assertEqual(doc_layout.logo, '')
+
+ def test_company_no_color_but_logo_change_logo(self):
+ """When company colors are not set, but a logo is,
+ the wizard displays the computed colors from the logo"""
+ self.company.write({
+ 'primary_color': '#ff0080',
+ 'secondary_color': '#00ff00',
+ 'logo': self.company_imgs['sweden']['img'],
+ 'paperformat_id': self.env.ref('base.paperformat_us').id,
+ })
+
+ with Form(self.env['base.document.layout']) as doc_layout:
+ self.assertColors(doc_layout, self.company)
+ doc_layout.logo = self.company_imgs['odoo']['img']
+ self.assertColors(doc_layout, self.company_imgs['odoo']['colors'])
+
+ def test_company_colors_change_logo(self):
+ """changes of the logo implies displaying the new computed colors"""
+ self.company.write({
+ 'primary_color': '#ff0080',
+ 'secondary_color': '#00ff00',
+ 'logo': False,
+ 'paperformat_id': self.env.ref('base.paperformat_us').id,
+ })
+
+ with Form(self.env['base.document.layout']) as doc_layout:
+ self.assertColors(doc_layout, self.company)
+ doc_layout.logo = self.company_imgs['odoo']['img']
+ self.assertColors(doc_layout, self.company_imgs['odoo']['colors'])
+
+ def test_company_colors_and_logo_change_logo(self):
+ """The colors of the company may differ from the one the logo computes
+ Opening the wizard in these condition displays the company's colors
+ When the logo changes, colors must change according to the logo"""
+ self.company.write({
+ 'primary_color': '#ff0080',
+ 'secondary_color': '#00ff00',
+ 'logo': self.company_imgs['sweden']['img'],
+ 'paperformat_id': self.env.ref('base.paperformat_us').id,
+ })
+
+ with Form(self.env['base.document.layout']) as doc_layout:
+ self.assertColors(doc_layout, self.company)
+ doc_layout.logo = self.company_imgs['odoo']['img']
+ self.assertColors(doc_layout, self.company_imgs['odoo']['colors'])
+
+ # Layout change tests
+ def test_company_colors_reset_colors(self):
+ """Reset the colors when they differ from the ones originally
+ computed from the company logo"""
+ self.company.write({
+ 'primary_color': '#ff0080',
+ 'secondary_color': '#00ff00',
+ 'logo': self.company_imgs['sweden']['img'],
+ 'paperformat_id': self.env.ref('base.paperformat_us').id,
+ })
+
+ with Form(self.env['base.document.layout']) as doc_layout:
+ self.assertColors(doc_layout, self.company)
+ doc_layout.primary_color = doc_layout.logo_primary_color
+ doc_layout.secondary_color = doc_layout.logo_secondary_color
+ self.assertColors(doc_layout, self.company_imgs['sweden']['colors'])
+
+ def test_parse_company_colors_grayscale(self):
+ """Grayscale images with transparency - make sure the color extraction does not crash"""
+ self.company.write({
+ 'primary_color': '#ff0080',
+ 'secondary_color': '#00ff00',
+ 'paperformat_id': self.env.ref('base.paperformat_us').id,
+ })
+ with Form(self.env['base.document.layout']) as doc_layout:
+ with Image.open(os.path.join(dir_path, 'logo_ci.png'), 'r') as img:
+ base64_img = image_to_base64(img, 'PNG')
+ doc_layout.logo = base64_img
+ self.assertNotEqual(None, doc_layout.primary_color)
+
+
+ # /!\ This case is NOT supported, and probably not supportable
+ # res.partner resizes manu-militari the image it is given
+ # so res.company._get_logo differs from res.partner.[default image]
+ # def test_company_no_colors_default_logo_and_layout_change_layout(self):
+ # """When the default YourCompany logo is set, and no colors are set on company:
+ # change wizard's color according to template"""
+ # self.company.write({
+ # 'primary_color': False,
+ # 'secondary_color': False,
+ # 'external_report_layout_id': self.layout_template1.id,
+ # })
+ # default_colors = self.default_colors
+ # with Form(self.env['base.document.layout']) as doc_layout:
+ # self.assertColors(doc_layout, default_colors)
+ # doc_layout.report_layout_id = self.report_layout2
+ # self.assertColors(doc_layout, self.report_layout2)
diff --git a/addons/web/tests/test_click_everywhere.py b/addons/web/tests/test_click_everywhere.py
new file mode 100644
index 00000000..92b2ad3c
--- /dev/null
+++ b/addons/web/tests/test_click_everywhere.py
@@ -0,0 +1,42 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import odoo.tests
+
+_logger = logging.getLogger(__name__)
+
+
+@odoo.tests.tagged('click_all', 'post_install', '-at_install', '-standard')
+class TestMenusAdmin(odoo.tests.HttpCase):
+
+ def test_01_click_everywhere_as_admin(self):
+ menus = self.env['ir.ui.menu'].load_menus(False)
+ for app in menus['children']:
+ with self.subTest(app=app['name']):
+ _logger.runbot('Testing %s', app['name'])
+ self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere']('%s');" % app['xmlid'], "odoo.isReady === true", login="admin", timeout=300)
+ self.terminate_browser()
+
+
+@odoo.tests.tagged('click_all', 'post_install', '-at_install', '-standard')
+class TestMenusDemo(odoo.tests.HttpCase):
+
+ def test_01_click_everywhere_as_demo(self):
+ menus = self.env['ir.ui.menu'].load_menus(False)
+ for app in menus['children']:
+ with self.subTest(app=app['name']):
+ _logger.runbot('Testing %s', app['name'])
+ self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere']('%s');" % app['xmlid'], "odoo.isReady === true", login="demo", timeout=300)
+ self.terminate_browser()
+
+@odoo.tests.tagged('post_install', '-at_install')
+class TestMenusAdminLight(odoo.tests.HttpCase):
+
+ def test_01_click_apps_menus_as_admin(self):
+ self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere'](undefined, true);", "odoo.isReady === true", login="admin", timeout=120)
+
+@odoo.tests.tagged('post_install', '-at_install',)
+class TestMenusDemoLight(odoo.tests.HttpCase):
+
+ def test_01_click_apps_menus_as_demo(self):
+ self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere'](undefined, true);", "odoo.isReady === true", login="demo", timeout=120)
diff --git a/addons/web/tests/test_image.py b/addons/web/tests/test_image.py
new file mode 100644
index 00000000..161a0645
--- /dev/null
+++ b/addons/web/tests/test_image.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import io
+import base64
+
+from PIL import Image
+
+from odoo.tests.common import HttpCase, tagged
+
+
+@tagged('-at_install', 'post_install')
+class TestImage(HttpCase):
+ def test_01_content_image_resize_placeholder(self):
+ """The goal of this test is to make sure the placeholder image is
+ resized appropriately depending on the given URL parameters."""
+
+ # CASE: resize placeholder, given size but original ratio is always kept
+ response = self.url_open('/web/image/0/200x150')
+ image = Image.open(io.BytesIO(response.content))
+ self.assertEqual(image.size, (150, 150))
+
+ # CASE: resize placeholder to 128
+ response = self.url_open('/web/image/fake/0/image_128')
+ image = Image.open(io.BytesIO(response.content))
+ self.assertEqual(image.size, (128, 128))
+
+ # CASE: resize placeholder to 256
+ response = self.url_open('/web/image/fake/0/image_256')
+ image = Image.open(io.BytesIO(response.content))
+ self.assertEqual(image.size, (256, 256))
+
+ # CASE: resize placeholder to 1024 (but placeholder image is too small)
+ response = self.url_open('/web/image/fake/0/image_1024')
+ image = Image.open(io.BytesIO(response.content))
+ self.assertEqual(image.size, (256, 256))
+
+ # CASE: no size found, use placeholder original size
+ response = self.url_open('/web/image/fake/0/image_no_size')
+ image = Image.open(io.BytesIO(response.content))
+ self.assertEqual(image.size, (256, 256))
+
+ def test_02_content_image_Etag_304(self):
+ """This test makes sure that the 304 response is properly returned if the ETag is properly set"""
+
+ attachment = self.env['ir.attachment'].create({
+ 'datas': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
+ 'name': 'testEtag.gif',
+ 'public': True,
+ 'mimetype': 'image/gif',
+ })
+ response = self.url_open('/web/image/%s' % attachment.id, timeout=None)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(base64.b64encode(response.content), attachment.datas)
+
+ etag = response.headers.get('ETag')
+
+ response2 = self.url_open('/web/image/%s' % attachment.id, headers={"If-None-Match": etag})
+ self.assertEqual(response2.status_code, 304)
+ self.assertEqual(len(response2.content), 0)
diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py
new file mode 100644
index 00000000..4d97e5f2
--- /dev/null
+++ b/addons/web/tests/test_js.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import re
+import odoo.tests
+
+RE_ONLY = re.compile('QUnit\.only\(')
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class WebSuite(odoo.tests.HttpCase):
+
+ def test_js(self):
+ # webclient desktop test suite
+ self.browser_js('/web/tests?mod=web&failfast', "", "", login='admin', timeout=1800)
+
+ def test_check_suite(self):
+ # verify no js test is using `QUnit.only` as it forbid any other test to be executed
+ self._check_only_call('web.qunit_suite_tests')
+ self._check_only_call('web.qunit_mobile_suite_tests')
+
+ def _check_only_call(self, suite):
+ # As we currently aren't in a request context, we can't render `web.layout`.
+ # redefinied it as a minimal proxy template.
+ self.env.ref('web.layout').write({'arch_db': '<t t-name="web.layout"><head><meta charset="utf-8"/><t t-raw="head"/></head></t>'})
+
+ for asset in self.env['ir.qweb']._get_asset_content(suite, options={})[0]:
+ filename = asset['filename']
+ if not filename or asset['atype'] != 'text/javascript':
+ continue
+ with open(filename, 'rb') as fp:
+ if RE_ONLY.search(fp.read().decode('utf-8')):
+ self.fail("`QUnit.only()` used in file %r" % asset['url'])
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class MobileWebSuite(odoo.tests.HttpCase):
+ browser_size = '375x667'
+
+ def test_mobile_js(self):
+ # webclient mobile test suite
+ self.browser_js('/web/tests/mobile?mod=web&failfast', "", "", login='admin', timeout=1800)
diff --git a/addons/web/tests/test_menu.py b/addons/web/tests/test_menu.py
new file mode 100644
index 00000000..d60721c1
--- /dev/null
+++ b/addons/web/tests/test_menu.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.tests.common import BaseCase
+from ..controllers import main
+
+
+class ActionMungerTest(BaseCase):
+ def test_actual_treeview(self):
+ action = {
+ "views": [[False, "tree"], [False, "form"],
+ [False, "calendar"]],
+ "view_type": "tree",
+ "view_id": False,
+ "view_mode": "tree,form,calendar"
+ }
+ changed = action.copy()
+ del action['view_type']
+ main.fix_view_modes(changed)
+
+ self.assertEqual(changed, action)
+
+ def test_list_view(self):
+ action = {
+ "views": [[False, "tree"], [False, "form"],
+ [False, "calendar"]],
+ "view_type": "form",
+ "view_id": False,
+ "view_mode": "tree,form,calendar"
+ }
+ main.fix_view_modes(action)
+
+ self.assertEqual(action, {
+ "views": [[False, "list"], [False, "form"],
+ [False, "calendar"]],
+ "view_id": False,
+ "view_mode": "list,form,calendar"
+ })
+
+ def test_redundant_views(self):
+
+ action = {
+ "views": [[False, "tree"], [False, "form"],
+ [False, "calendar"], [42, "tree"]],
+ "view_type": "form",
+ "view_id": False,
+ "view_mode": "tree,form,calendar"
+ }
+ main.fix_view_modes(action)
+
+ self.assertEqual(action, {
+ "views": [[False, "list"], [False, "form"],
+ [False, "calendar"], [42, "list"]],
+ "view_id": False,
+ "view_mode": "list,form,calendar"
+ })
diff --git a/addons/web/tests/test_read_progress_bar.py b/addons/web/tests/test_read_progress_bar.py
new file mode 100644
index 00000000..ab5eaf1c
--- /dev/null
+++ b/addons/web/tests/test_read_progress_bar.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.tests import common
+
+
+@common.tagged('post_install', '-at_install')
+class TestReadProgressBar(common.TransactionCase):
+ """Test for read_progress_bar"""
+
+ def setUp(self):
+ super(TestReadProgressBar, self).setUp()
+ self.Model = self.env['res.partner']
+
+ def test_week_grouping(self):
+ """The labels associated to each record in read_progress_bar should match
+ the ones from read_group, even in edge cases like en_US locale on sundays
+ """
+ context = {"lang": "en_US"}
+ groupby = "date:week"
+ self.Model.create({'date': '2021-05-02', 'name': "testWeekGrouping_first"}) # Sunday
+ self.Model.create({'date': '2021-05-09', 'name': "testWeekGrouping_second"}) # Sunday
+ progress_bar = {
+ 'field': 'name',
+ 'colors': {
+ "testWeekGrouping_first": 'success',
+ "testWeekGrouping_second": 'danger',
+ }
+ }
+
+ groups = self.Model.with_context(context).read_group(
+ [('name', "like", "testWeekGrouping%")], fields=['date', 'name'], groupby=[groupby])
+ progressbars = self.Model.with_context(context).read_progress_bar(
+ [('name', "like", "testWeekGrouping%")], group_by=groupby, progress_bar=progress_bar)
+ self.assertEqual(len(groups), 2)
+ self.assertEqual(len(progressbars), 2)
+
+ # format the read_progress_bar result to get a dictionary under this format : {record_name: group_name}
+ # original format (after read_progress_bar) is : {group_name: {record_name: count}}
+ pg_groups = {
+ next(record_name for record_name, count in data.items() if count): group_name \
+ for group_name, data in progressbars.items()
+ }
+
+ self.assertEqual(groups[0][groupby], pg_groups["testWeekGrouping_first"])
+ self.assertEqual(groups[1][groupby], pg_groups["testWeekGrouping_second"])
+
+ def test_simple(self):
+ model = self.env['ir.model'].create({
+ 'model': 'x_progressbar',
+ 'name': 'progress_bar',
+ 'field_id': [
+ (0, 0, {
+ 'field_description': 'Country',
+ 'name': 'x_country_id',
+ 'ttype': 'many2one',
+ 'relation': 'res.country',
+ }),
+ (0, 0, {
+ 'field_description': 'Date',
+ 'name': 'x_date',
+ 'ttype': 'date',
+ }),
+ (0, 0, {
+ 'field_description': 'State',
+ 'name': 'x_state',
+ 'ttype': 'selection',
+ 'selection': "[('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz')]",
+ }),
+ ],
+ })
+
+ c1, c2, c3 = self.env['res.country'].search([], limit=3)
+
+ self.env['x_progressbar'].create([
+ # week 21
+ {'x_country_id': c1.id, 'x_date': '2021-05-20', 'x_state': 'foo'},
+ {'x_country_id': c1.id, 'x_date': '2021-05-21', 'x_state': 'foo'},
+ {'x_country_id': c1.id, 'x_date': '2021-05-22', 'x_state': 'foo'},
+ {'x_country_id': c1.id, 'x_date': '2021-05-23', 'x_state': 'bar'},
+ # week 22
+ {'x_country_id': c1.id, 'x_date': '2021-05-24', 'x_state': 'baz'},
+ {'x_country_id': c2.id, 'x_date': '2021-05-25', 'x_state': 'foo'},
+ {'x_country_id': c2.id, 'x_date': '2021-05-26', 'x_state': 'bar'},
+ {'x_country_id': c2.id, 'x_date': '2021-05-27', 'x_state': 'bar'},
+ {'x_country_id': c2.id, 'x_date': '2021-05-28', 'x_state': 'baz'},
+ {'x_country_id': c2.id, 'x_date': '2021-05-29', 'x_state': 'baz'},
+ {'x_country_id': c3.id, 'x_date': '2021-05-30', 'x_state': 'foo'},
+ # week 23
+ {'x_country_id': c3.id, 'x_date': '2021-05-31', 'x_state': 'foo'},
+ {'x_country_id': c3.id, 'x_date': '2021-06-01', 'x_state': 'baz'},
+ {'x_country_id': c3.id, 'x_date': '2021-06-02', 'x_state': 'baz'},
+ {'x_country_id': c3.id, 'x_date': '2021-06-03', 'x_state': 'baz'},
+ ])
+
+ progress_bar = {
+ 'field': 'x_state',
+ 'colors': {'foo': 'success', 'bar': 'warning', 'baz': 'danger'},
+ }
+ result = self.env['x_progressbar'].read_progress_bar([], 'x_country_id', progress_bar)
+ self.assertEqual(result, {
+ c1.display_name: {'foo': 3, 'bar': 1, 'baz': 1},
+ c2.display_name: {'foo': 1, 'bar': 2, 'baz': 2},
+ c3.display_name: {'foo': 2, 'bar': 0, 'baz': 3},
+ })
+
+ # check date aggregation and format
+ result = self.env['x_progressbar'].read_progress_bar([], 'x_date:week', progress_bar)
+ self.assertEqual(result, {
+ 'W21 2021': {'foo': 3, 'bar': 1, 'baz': 0},
+ 'W22 2021': {'foo': 2, 'bar': 2, 'baz': 3},
+ 'W23 2021': {'foo': 1, 'bar': 0, 'baz': 3},
+ })
+
+ # add a computed field on model
+ model.write({'field_id': [
+ (0, 0, {
+ 'field_description': 'Related State',
+ 'name': 'x_state_computed',
+ 'ttype': 'selection',
+ 'selection': "[('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz')]",
+ 'compute': "for rec in self: rec['x_state_computed'] = rec.x_state",
+ 'depends': 'x_state',
+ 'readonly': True,
+ 'store': False,
+ }),
+ ]})
+
+ progress_bar = {
+ 'field': 'x_state_computed',
+ 'colors': {'foo': 'success', 'bar': 'warning', 'baz': 'danger'},
+ }
+ result = self.env['x_progressbar'].read_progress_bar([], 'x_country_id', progress_bar)
+ self.assertEqual(result, {
+ c1.display_name: {'foo': 3, 'bar': 1, 'baz': 1},
+ c2.display_name: {'foo': 1, 'bar': 2, 'baz': 2},
+ c3.display_name: {'foo': 2, 'bar': 0, 'baz': 3},
+ })
+
+ result = self.env['x_progressbar'].read_progress_bar([], 'x_date:week', progress_bar)
+ self.assertEqual(result, {
+ 'W21 2021': {'foo': 3, 'bar': 1, 'baz': 0},
+ 'W22 2021': {'foo': 2, 'bar': 2, 'baz': 3},
+ 'W23 2021': {'foo': 1, 'bar': 0, 'baz': 3},
+ })
diff --git a/addons/web/tests/test_serving_base.py b/addons/web/tests/test_serving_base.py
new file mode 100644
index 00000000..d7d83c25
--- /dev/null
+++ b/addons/web/tests/test_serving_base.py
@@ -0,0 +1,1002 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import random
+import re
+from unittest.mock import patch
+import textwrap
+from datetime import datetime, timedelta
+from lxml import etree
+import logging
+
+from odoo.tests.common import BaseCase, tagged
+from odoo.tools import topological_sort
+from odoo.addons.web.controllers.main import HomeStaticTemplateHelpers
+
+_logger = logging.getLogger(__name__)
+
+def sample(population):
+ return random.sample(
+ population,
+ random.randint(0, min(len(population), 5)))
+
+
+class TestModulesLoading(BaseCase):
+ def setUp(self):
+ self.mods = [str(i) for i in range(1000)]
+
+ def test_topological_sort(self):
+ random.shuffle(self.mods)
+ modules = [
+ (k, sample(self.mods[:i]))
+ for i, k in enumerate(self.mods)]
+ random.shuffle(modules)
+ ms = dict(modules)
+
+ seen = set()
+ sorted_modules = topological_sort(ms)
+ for module in sorted_modules:
+ deps = ms[module]
+ self.assertGreaterEqual(
+ seen, set(deps),
+ 'Module %s (index %d), ' \
+ 'missing dependencies %s from loaded modules %s' % (
+ module, sorted_modules.index(module), deps, seen
+ ))
+ seen.add(module)
+
+
+class TestStaticInheritanceCommon(BaseCase):
+
+ def setUp(self):
+ super(TestStaticInheritanceCommon, 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(TestStaticInheritanceCommon, 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)()
+
+
+@tagged('static_templates')
+class TestStaticInheritance(TestStaticInheritanceCommon):
+ # 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">
+ <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_02(self):
+ 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>
+ <form t-name="template_1_2" t-inherit="template_1_1" added="true">
+ <xpath expr="//div[1]" position="after">
+ <div>I was petrified</div>
+ </xpath>
+ </form>
+ </templates>
+ '''
+ }
+ self.modules = [
+ ('module_1_file_1', None, 'module_1'),
+ ]
+ 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>
+ <form t-name="template_1_2" random-attr="gloria" added="true">
+ <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_03(self):
+ self.maxDiff = None
+ 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" added="true">
+ <xpath expr="//div[1]" position="after">
+ <div>I was petrified</div>
+ </xpath>
+ </form>
+ <form t-name="template_1_3" t-inherit="template_1_2" added="false" other="here">
+ <xpath expr="//div[2]" position="replace"/>
+ </form>
+ </templates>
+ '''
+ }
+ self.modules = [
+ ('module_1_file_1', None, 'module_1'),
+ ]
+ 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" added="true">
+ <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>
+ <form t-name="template_1_3" added="false" other="here">
+ <div>At first I was afraid</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_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">
+ <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">
+ <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">
+ <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 test_replace_in_debug_mode(self):
+ """
+ Replacing a template's meta definition in place doesn't keep the original attrs of the template
+ """
+ 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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="extension">
+ <xpath expr="." position="replace">
+ <div overriden-attr="overriden">And I grew strong</div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <div overriden-attr="overriden" t-name="template_1_1">
+ <!-- Modified by template_1_2 from module_1 -->And I grew strong
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_replace_in_debug_mode2(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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="extension">
+ <xpath expr="." position="replace">
+ <div>
+ And I grew strong
+ <p>And I learned how to get along</p>
+ And so you're back
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <div t-name="template_1_1">
+ <!-- Modified by template_1_2 from module_1 -->
+ And I grew strong
+ <p>And I learned how to get along</p>
+ And so you're back
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_replace_in_debug_mode3(self):
+ """Text outside of a div which will replace a whole template
+ becomes outside of the template
+ This doesn't mean anything in terms of the business of template inheritance
+ But it is in the XPATH specs"""
+ 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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="extension">
+ <xpath expr="." position="replace">
+ <div>
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ And so you're back
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <div t-name="template_1_1">
+ <!-- Modified by template_1_2 from module_1 -->
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ And so you're back
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_replace_root_node_tag(self):
+ """
+ Root node IS targeted by //NODE_TAG in xpath
+ """
+ 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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ <form>Inner Form</form>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="extension">
+ <xpath expr="//form" position="replace">
+ <div>
+ Form replacer
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <div t-name="template_1_1">
+ <!-- Modified by template_1_2 from module_1 -->
+ Form replacer
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_replace_root_node_tag_in_primary(self):
+ """
+ Root node IS targeted by //NODE_TAG in xpath
+ """
+ self.maxDiff = None
+ 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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ <form>Inner Form</form>
+ </form>
+ <form t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="primary">
+ <xpath expr="//form" position="replace">
+ <div>Form replacer</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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ <form>Inner Form</form>
+ </form>
+ <div t-name="template_1_2">
+ Form replacer
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_inherit_primary_replace_debug(self):
+ """
+ The inheriting template has got both its own defining attrs
+ and new ones if one is to replace its defining root node
+ """
+ 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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="primary">
+ <xpath expr="." position="replace">
+ <div overriden-attr="overriden">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ 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>
+ </form>
+ <div overriden-attr="overriden" t-name="template_1_2">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_replace_in_nodebug_mode1(self):
+ """Comments already in the arch are ignored"""
+ 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" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1" t-inherit-mode="extension">
+ <xpath expr="." position="replace">
+ <div>
+ <!-- Random Comment -->
+ And I grew strong
+ <p>And I learned how to get along</p>
+ And so you're back
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=False)
+ expected = b"""
+ <templates>
+ <div t-name="template_1_1">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ And so you're back
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_inherit_from_dotted_tname_1(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="module_1.template_1_1.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1.dot" t-inherit-mode="primary">
+ <xpath expr="." position="replace">
+ <div overriden-attr="overriden">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="module_1.template_1_1.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <div overriden-attr="overriden" t-name="template_1_2">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_inherit_from_dotted_tname_2(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.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="template_1_1.dot" t-inherit-mode="primary">
+ <xpath expr="." position="replace">
+ <div overriden-attr="overriden">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="template_1_1.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <div overriden-attr="overriden" t-name="template_1_2">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_inherit_from_dotted_tname_2bis(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.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="module_1.template_1_1.dot" t-inherit-mode="primary">
+ <xpath expr="." position="replace">
+ <div overriden-attr="overriden">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="template_1_1.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <div overriden-attr="overriden" t-name="template_1_2">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_inherit_from_dotted_tname_2ter(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="module_1" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <t t-name="template_1_2" t-inherit="module_1" t-inherit-mode="primary">
+ <xpath expr="." position="replace">
+ <div overriden-attr="overriden">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """,
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="module_1" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <div overriden-attr="overriden" t-name="template_1_2">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+ def test_inherit_from_dotted_tname_3(self):
+ 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="module_1.template_1_1.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ </templates>
+ """,
+
+ 'module_2_file_1': b"""
+ <templates id="template" xml:space="preserve">
+ <t t-name="template_2_1" t-inherit="module_1.template_1_1.dot" t-inherit-mode="primary">
+ <xpath expr="." position="replace">
+ <div overriden-attr="overriden">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </xpath>
+ </t>
+ </templates>
+ """
+ }
+
+ contents = HomeStaticTemplateHelpers.get_qweb_templates(addons=self._get_module_names(), debug=True)
+ expected = b"""
+ <templates>
+ <form t-name="module_1.template_1_1.dot" random-attr="gloria">
+ <div>At first I was afraid</div>
+ </form>
+ <div overriden-attr="overriden" t-name="template_2_1">
+ And I grew strong
+ <p>And I learned how to get along</p>
+ </div>
+ </templates>
+ """
+
+ self.assertXMLEqual(contents, expected)
+
+
+@tagged('-standard', 'static_templates_performance')
+class TestStaticInheritancePerformance(TestStaticInheritanceCommon):
+ 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/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/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_static_templates_treatment_linearity(self):
+ # With 2500 templates for starters
+ 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()
+ delta2500 = after - before
+ _logger.runbot('Static Templates Inheritance: 2500 templates treated in %s seconds' % delta2500.total_seconds())
+
+ whole_tree = etree.fromstring(contents)
+ self.assertEqual(len(whole_tree), nMod * nFilePerMod * nTemplatePerFile)
+
+ # With 25000 templates next
+ 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()
+ delta25000 = after - before
+
+ time_ratio = delta25000.total_seconds() / delta2500.total_seconds()
+ _logger.runbot('Static Templates Inheritance: 25000 templates treated in %s seconds' % delta25000.total_seconds())
+ _logger.runbot('Static Templates Inheritance: Computed linearity ratio: %s' % time_ratio)
+ self.assertLessEqual(time_ratio, 12)