mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 10:32:07 +02:00
vanilla 18.0
This commit is contained in:
parent
0a7ae8db93
commit
5454004ff9
1963 changed files with 1187893 additions and 919508 deletions
|
|
@ -5,17 +5,22 @@ from . import test_health
|
|||
from . import test_image
|
||||
from . import test_ir_model
|
||||
from . import test_js
|
||||
from . import test_menu
|
||||
from . import test_router
|
||||
from . import test_click_everywhere
|
||||
from . import test_base_document_layout
|
||||
from . import test_load_menus
|
||||
from . import test_partner
|
||||
from . import test_profiler
|
||||
from . import test_session_info
|
||||
from . import test_read_progress_bar
|
||||
from . import test_assets
|
||||
from . import test_assets_xml
|
||||
from . import test_login
|
||||
from . import test_web_search_read
|
||||
from . import test_web_read_group
|
||||
from . import test_domain
|
||||
from . import test_translate
|
||||
from . import test_web_redirect
|
||||
from . import test_res_users
|
||||
from . import test_webmanifest
|
||||
from . import test_ir_qweb
|
||||
from . import test_reports
|
||||
from . import test_pivot_export
|
||||
|
|
|
|||
|
|
@ -17,26 +17,31 @@ _logger = logging.getLogger(__name__)
|
|||
|
||||
class TestAssetsGenerateTimeCommon(odoo.tests.TransactionCase):
|
||||
|
||||
def generate_bundles(self):
|
||||
self.env['ir.attachment'].search([('url', '=like', '/web/assets/%')]).unlink() # delete existing attachement
|
||||
def generate_bundles(self, unlink=True):
|
||||
if unlink:
|
||||
self.env['ir.attachment'].search([('url', '=like', '/web/assets/%')]).unlink() # delete existing attachement
|
||||
installed_module_names = self.env['ir.module.module'].search([('state', '=', 'installed')]).mapped('name')
|
||||
bundles = {
|
||||
key
|
||||
for module in installed_module_names
|
||||
for key in get_manifest(module)['assets']
|
||||
for key in get_manifest(module).get('assets', [])
|
||||
}
|
||||
|
||||
for bundle in bundles:
|
||||
for bundle_name in bundles:
|
||||
with mute_logger('odoo.addons.base.models.assetsbundle'):
|
||||
for assets_type in 'css', 'js':
|
||||
try:
|
||||
start_t = time.time()
|
||||
css = assets_type == 'css'
|
||||
js = assets_type == 'js'
|
||||
self.env['ir.qweb']._generate_asset_nodes(bundle, css=css, js=js)
|
||||
yield (f'{bundle}.{assets_type}', time.time() - start_t)
|
||||
bundle = self.env['ir.qweb']._get_asset_bundle(bundle_name, css=css, js=js)
|
||||
if assets_type == 'css' and bundle.stylesheets:
|
||||
bundle.css()
|
||||
if assets_type == 'js' and bundle.javascripts:
|
||||
bundle.js()
|
||||
yield (f'{bundle_name}.{assets_type}', time.time() - start_t)
|
||||
except ValueError:
|
||||
_logger.info('Error detected while generating bundle %r %s', bundle, assets_type)
|
||||
_logger.info('Error detected while generating bundle %r %s', bundle_name, assets_type)
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install', 'assets_bundle')
|
||||
|
|
@ -47,9 +52,33 @@ class TestLogsAssetsGenerateTime(TestAssetsGenerateTimeCommon):
|
|||
The purpose of this test is to monitor the time of assets bundle generation.
|
||||
This is not meant to test the generation failure, hence the try/except and the mute logger.
|
||||
"""
|
||||
for bundle, duration in self.generate_bundles():
|
||||
for bundle, duration in list(self.generate_bundles()):
|
||||
_logger.info('Bundle %r generated in %.2fs', bundle, duration)
|
||||
|
||||
def test_logs_assets_check_time(self):
|
||||
"""
|
||||
The purpose of this test is to monitor the time of assets bundle generation.
|
||||
This is not meant to test the generation failure, hence the try/except and the mute logger.
|
||||
"""
|
||||
start = time.time()
|
||||
for bundle, duration in self.generate_bundles(False):
|
||||
_logger.info('Bundle %r checked in %.2fs', bundle, duration)
|
||||
duration = time.time() - start
|
||||
_logger.info('All bundle checked in %.2fs', duration)
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'test_assets')
|
||||
class TestPregenerateTime(HttpCase):
|
||||
|
||||
def test_logs_pregenerate_time(self):
|
||||
self.env['ir.qweb']._pregenerate_assets_bundles()
|
||||
start = time.time()
|
||||
self.env.registry.clear_cache()
|
||||
self.env.cache.invalidate()
|
||||
with self.profile(collectors=['sql', odoo.tools.profiler.PeriodicCollector(interval=0.01)], disable_gc=True):
|
||||
self.env['ir.qweb']._pregenerate_assets_bundles()
|
||||
duration = time.time() - start
|
||||
_logger.info('All bundle checked in %.2fs', duration)
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'assets_bundle')
|
||||
class TestAssetsGenerateTime(TestAssetsGenerateTimeCommon):
|
||||
|
|
@ -73,6 +102,7 @@ class TestAssetsGenerateTime(TestAssetsGenerateTimeCommon):
|
|||
class TestLoad(HttpCase):
|
||||
def test_assets_already_exists(self):
|
||||
self.authenticate('admin', 'admin')
|
||||
# TODO xdo adapt this test. url open won't generate attachment anymore even if not pregenerated
|
||||
_save_attachment = odoo.addons.base.models.assetsbundle.AssetsBundle.save_attachment
|
||||
|
||||
def save_attachment(bundle, extension, content):
|
||||
|
|
@ -82,5 +112,83 @@ class TestLoad(HttpCase):
|
|||
return attachment
|
||||
|
||||
with patch('odoo.addons.base.models.assetsbundle.AssetsBundle.save_attachment', save_attachment):
|
||||
self.url_open('/web').raise_for_status()
|
||||
self.url_open('/odoo').raise_for_status()
|
||||
self.url_open('/').raise_for_status()
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestWebAssetsCursors(HttpCase):
|
||||
"""
|
||||
This tests class tests the specificities of the route /web/assets regarding used connections.
|
||||
|
||||
The route is almost always read-only, except when the bundle is missing/outdated.
|
||||
To avoid retrying in all cases on the first request after an update/change, the route
|
||||
uses a cursor to check if the bundle is up-to-date, then opens a new cursor to generate
|
||||
the bundle if needed.
|
||||
|
||||
This optimization is only possible because the route has a simple flow: check, generate, return.
|
||||
No other operation is done on the database in between.
|
||||
We don't want to open another cursor to generate the bundle if the check is done with a read/write
|
||||
cursor, if we don't have a replica.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.bundle_name = 'web.assets_frontend'
|
||||
cls.bundle_version = cls.env['ir.qweb']._get_asset_bundle(cls.bundle_name).get_version('css')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env['ir.attachment'].search([('url', '=like', '/web/assets/%')]).unlink()
|
||||
self.bundle_name = 'web.assets_frontend'
|
||||
|
||||
def _get_generate_cursors_readwriteness(self):
|
||||
"""
|
||||
This method returns the list cursors read-writness used to generate the bundle
|
||||
:returns: [('ro|rw', '(ro_requested|rw_requested)')]
|
||||
"""
|
||||
cursors = []
|
||||
original_cursor = self.env.registry.cursor
|
||||
def cursor(readonly=False):
|
||||
cursor = original_cursor(readonly=readonly)
|
||||
cursors.append(('ro' if cursor.readonly else 'rw', '(ro_requested)' if readonly else '(rw_requested)'))
|
||||
return cursor
|
||||
|
||||
with patch.object(self.env.registry, 'cursor', cursor):
|
||||
response = self.url_open(f'/web/assets/{self.bundle_version}/{self.bundle_name}.min.css', allow_redirects=False)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# remove the check_signaling cursor
|
||||
self.assertEqual(cursors[0][1], '(rw_requested)', "the first cursor used for match and check signaling should be rw")
|
||||
return cursors[1:]
|
||||
|
||||
def test_web_binary_keep_cursor_ro(self):
|
||||
"""
|
||||
With replica, will need two cursors for generation, then a read-only cursor for all other call
|
||||
"""
|
||||
self.assertEqual(
|
||||
self._get_generate_cursors_readwriteness(),
|
||||
[
|
||||
('ro', '(ro_requested)'),
|
||||
('rw', '(rw_requested)'),
|
||||
],
|
||||
'A ro and rw cursor should be used to generate assets without replica when cold',
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self._get_generate_cursors_readwriteness(),
|
||||
[
|
||||
('ro', '(ro_requested)'),
|
||||
],
|
||||
'Only one readonly cursor should be used to generate assets wit replica when warm',
|
||||
)
|
||||
|
||||
def test_web_binary_keep_cursor_rw(self):
|
||||
self.env.registry.test_readonly_enabled = False
|
||||
self.assertEqual(
|
||||
self._get_generate_cursors_readwriteness(),
|
||||
[
|
||||
('rw', '(ro_requested)'),
|
||||
],
|
||||
'Only one readwrite cursor should be used to generate assets without replica',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,933 +0,0 @@
|
|||
# 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
|
||||
from lxml import etree
|
||||
import logging
|
||||
|
||||
import odoo
|
||||
from odoo.tests.common import BaseCase, HttpCase, tagged
|
||||
from odoo.tools import topological_sort
|
||||
from odoo.addons.base.models.assetsbundle import AssetsBundle, WebAsset
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class TestStaticInheritanceCommon(odoo.tests.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<span>Ho !</span>
|
||||
<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/static/xml/file_1.xml': """
|
||||
<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="//span" position="attributes">
|
||||
<attribute name="type">Scary screams</attribute>
|
||||
</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._patch = patch.object(WebAsset, '_fetch_content', lambda asset: self.template_files[asset.url])
|
||||
self.startPatcher(self._patch)
|
||||
|
||||
def renderBundle(self, debug=False):
|
||||
files = []
|
||||
for url in self.template_files:
|
||||
atype = 'text/xml'
|
||||
if '.js' in url:
|
||||
atype = 'text/javascript'
|
||||
files.append({
|
||||
'atype': atype,
|
||||
'url': url,
|
||||
'filename': url,
|
||||
'content': None,
|
||||
'media': None,
|
||||
})
|
||||
asset = AssetsBundle('web.test_bundle', files, env=self.env, css=False, js=True)
|
||||
# to_node return the files descriptions and generate attachments.
|
||||
asset.to_node(css=False, js=False, debug=debug and 'assets' or '')
|
||||
content = asset.xml(show_inherit_info=debug)
|
||||
return f'<templates xml:space="preserve">\n{content}\n</templates>'
|
||||
|
||||
# Custom Assert
|
||||
def assertXMLEqual(self, output, expected):
|
||||
self.assertTrue(output)
|
||||
self.assertTrue(expected)
|
||||
self.assertEqual(etree.fromstring(output), etree.fromstring(expected))
|
||||
|
||||
@tagged('assets_bundle', 'static_templates')
|
||||
class TestStaticInheritance(TestStaticInheritanceCommon):
|
||||
# Actual test cases
|
||||
def test_static_with_debug_mode(self):
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Filepath: /module_1/static/xml/file_1.xml -->
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<span>Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<div>Kept thinking I could never live without you by my side</div>
|
||||
</form>
|
||||
|
||||
<!-- Filepath: /module_1/static/xml/file_1.xml => /module_2/static/xml/file_1.xml -->
|
||||
<t t-name="template_1_2">
|
||||
<div>And I grew strong</div>
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml ; position="after" ; {'expr': '//div[1]'} --><div>And I learned how to get along</div>
|
||||
</t>
|
||||
|
||||
<!-- Filepath: /module_1/static/xml/file_1.xml => /module_2/static/xml/file_1.xml -->
|
||||
<form t-name="template_2_1" random-attr="gloria"><!-- Filepath: /module_2/static/xml/file_1.xml ; position="attributes" ; {'expr': '//span'} -->
|
||||
<span type="Scary screams">Ho !</span>
|
||||
<div>At first I was afraid</div>
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml ; position="after" ; {'expr': '//div[1]'} --><div>I was petrified</div>
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml ; position="after" ; {'expr': '//div[2]'} --><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>
|
||||
|
||||
<!-- Filepath: /module_2/static/xml/file_1.xml -->
|
||||
<div t-name="template_2_2">
|
||||
<div>And I learned how to get along</div>
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=True), expected)
|
||||
|
||||
def test_static_inheritance_01(self):
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<span>Ho !</span>
|
||||
<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>
|
||||
<div>And I learned how to get along</div>
|
||||
</t>
|
||||
<form t-name="template_2_1" random-attr="gloria">
|
||||
<span type="Scary screams">Ho !</span>
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_02(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates 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" 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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_03(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<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>
|
||||
'''
|
||||
}
|
||||
expected = """
|
||||
<templates 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" 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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_in_same_module(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<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/static/xml/file_2.xml': '''
|
||||
<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>
|
||||
'''
|
||||
}
|
||||
expected = """
|
||||
<templates 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">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inheritance_in_same_file(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<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>
|
||||
''',
|
||||
}
|
||||
expected = """
|
||||
<templates 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">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_inherit_extended_template(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<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>
|
||||
''',
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="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>
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_sibling_extension(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': '''
|
||||
<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/static/xml/file_1.xml': '''
|
||||
<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/static/xml/file_1.xml': '''
|
||||
<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>
|
||||
'''
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1">
|
||||
<div>I am a man of constant sorrow</div>
|
||||
<div>In constant sorrow all through his days</div>
|
||||
<div>Oh Brother !</div>
|
||||
<div>I've seen trouble all my days</div>
|
||||
</form>
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_static_misordered_modules(self):
|
||||
files = self.template_files
|
||||
self.template_files = {
|
||||
'/module_2/static/xml/file_1.xml': files['/module_2/static/xml/file_1.xml'],
|
||||
'/module_1/static/xml/file_1.xml': files['/module_1/static/xml/file_1.xml'],
|
||||
}
|
||||
with self.assertRaises(ValueError) as ve:
|
||||
self.renderBundle(debug=False)
|
||||
|
||||
self.assertEqual(
|
||||
str(ve.exception),
|
||||
"Module 'module_1' not loaded or inexistent (try to inherit 'template_1_1'), or templates of addon being loaded 'module_2' are misordered (template 'template_2_1')"
|
||||
)
|
||||
|
||||
def test_static_misordered_templates(self):
|
||||
self.template_files['/module_2/static/xml/file_1.xml'] = """
|
||||
<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:
|
||||
self.renderBundle(debug=False)
|
||||
|
||||
self.assertEqual(
|
||||
str(ve.exception),
|
||||
"Cannot create 'module_2.template_2_1' because the template to inherit 'module_2.template_2_2' is not found.",
|
||||
)
|
||||
|
||||
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.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div overriden-attr="overriden" t-name="template_1_1">
|
||||
And I grew strong
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_in_debug_mode2(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), 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.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="template_1_1">
|
||||
And I grew strong
|
||||
<p>And I learned how to get along</p>
|
||||
</div>
|
||||
And so you're back
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_root_node_tag(self):
|
||||
"""
|
||||
Root node IS targeted by //NODE_TAG in xpath
|
||||
"""
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<div t-name="template_1_1">
|
||||
Form replacer
|
||||
</div>
|
||||
</templates>
|
||||
"""
|
||||
self.assertXMLEqual(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_root_node_tag_in_primary(self):
|
||||
"""
|
||||
Root node IS targeted by //NODE_TAG in xpath
|
||||
"""
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), 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.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_replace_in_nodebug_mode1(self):
|
||||
"""Comments already in the arch are ignored"""
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_1(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_2(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_2bis(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_2ter(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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>
|
||||
""",
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_from_dotted_tname_3(self):
|
||||
self.template_files = {
|
||||
'/module_1/static/xml/file_1.xml': """
|
||||
<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/static/xml/file_1.xml': """
|
||||
<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>
|
||||
"""
|
||||
}
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
def test_inherit_and_qweb_extend(self):
|
||||
self.template_files['/module_1/static/xml/file_2.xml'] = """
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="template_qw_1">
|
||||
<div>111</div>
|
||||
</t>
|
||||
<form t-inherit="template_1_1" t-inherit-mode="extension">
|
||||
<xpath expr="//span[1]" position="replace">
|
||||
<article>!!!</article>
|
||||
</xpath>
|
||||
</form>
|
||||
<t t-name="template_qw_2">
|
||||
<div>222</div>
|
||||
</t>
|
||||
<t t-extend="template_qw_1">
|
||||
<t t-jquery="div" t-operation="after">
|
||||
<div>333</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
"""
|
||||
|
||||
expected = """
|
||||
<templates xml:space="preserve">
|
||||
<form t-name="template_1_1" random-attr="gloria">
|
||||
<article>!!!</article>
|
||||
<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>
|
||||
<div>And I learned how to get along</div>
|
||||
</t>
|
||||
<t t-name="template_qw_1">
|
||||
<div>111</div>
|
||||
</t>
|
||||
<t t-name="template_qw_2">
|
||||
<div>222</div>
|
||||
</t>
|
||||
<t t-extend="template_qw_1">
|
||||
<t t-jquery="div" t-operation="after">
|
||||
<div>333</div>
|
||||
</t>
|
||||
</t>
|
||||
<form t-name="template_2_1" random-attr="gloria">
|
||||
<span type="Scary screams">Ho !</span>
|
||||
<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(self.renderBundle(debug=False), expected)
|
||||
|
||||
|
||||
@tagged('-standard', 'assets_bundle', '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.asset_paths = []
|
||||
self.template_files = {}
|
||||
number_templates = 0
|
||||
for m in range(nMod):
|
||||
for f in range(nFilePerMod):
|
||||
mname = 'mod_%s' % m
|
||||
fname = 'mod_%s/folder/file_%s.xml' % (m, f)
|
||||
self.asset_paths.append((fname, mname, 'bundle_1'))
|
||||
|
||||
_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
|
||||
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 = self.renderBundle(debug=False)
|
||||
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()
|
||||
self.renderBundle(debug=False)
|
||||
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, 14)
|
||||
|
|
@ -3,7 +3,8 @@ 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
|
||||
from odoo.tools import frozendict
|
||||
from odoo.tools.image import image_to_base64, hex_to_rgb
|
||||
|
||||
|
||||
dir_path = os.path.dirname(os.path.realpath(__file__))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
|
||||
import logging
|
||||
import odoo.tests
|
||||
|
||||
from requests import Session, PreparedRequest, Response
|
||||
|
||||
from datetime import datetime
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -15,36 +20,71 @@ class TestMenusAdmin(odoo.tests.HttpCase):
|
|||
for app_id in menus['root']['children']:
|
||||
with self.subTest(app=menus[app_id]['name']):
|
||||
_logger.runbot('Testing %s', menus[app_id]['name'])
|
||||
self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere']('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="admin", timeout=600)
|
||||
self.terminate_browser()
|
||||
self.browser_js("/odoo", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="admin", timeout=1200, success_signal="clickbot test succeeded")
|
||||
|
||||
|
||||
@odoo.tests.tagged('click_all', 'post_install', '-at_install', '-standard')
|
||||
class TestMenusDemo(odoo.tests.HttpCase):
|
||||
class TestMenusDemo(HttpCaseWithUserDemo):
|
||||
allow_end_on_form = True
|
||||
def test_01_click_everywhere_as_demo(self):
|
||||
user_demo = self.env.ref("base.user_demo")
|
||||
user_demo = self.user_demo
|
||||
menus = self.env['ir.ui.menu'].with_user(user_demo.id).load_menus(False)
|
||||
for app_id in menus['root']['children']:
|
||||
with self.subTest(app=menus[app_id]['name']):
|
||||
_logger.runbot('Testing %s', menus[app_id]['name'])
|
||||
self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere']('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="demo", timeout=600)
|
||||
self.terminate_browser()
|
||||
self.browser_js("/odoo", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="demo", timeout=1200, success_signal="clickbot test succeeded")
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestMenusAdminLight(odoo.tests.HttpCase):
|
||||
allow_end_on_form = True
|
||||
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',)
|
||||
@classmethod
|
||||
def _request_handler(cls, s: Session, r: PreparedRequest, /, **kw):
|
||||
# mock odoofin requests
|
||||
if 'proxy/v1/get_dashboard_institutions' in r.url:
|
||||
r = Response()
|
||||
r.status_code = 200
|
||||
r.json = lambda: {'result': {}}
|
||||
return r
|
||||
return super()._request_handler(s, r, **kw)
|
||||
|
||||
def test_01_click_apps_menus_as_admin(self):
|
||||
# Disable onboarding tours to remove warnings
|
||||
if 'tour_enabled' in self.env['res.users']._fields:
|
||||
self.env.ref('base.user_admin').tour_enabled = False
|
||||
# Due to action_pos_preparation_display_kitchen_display, cliking on the "Kitchen Display"
|
||||
# menuitem could open the UI display, which will break the crawler tests as there is no
|
||||
# way for the tour to be executed, leading to a timeout
|
||||
if 'pos_preparation_display.display' in self.env:
|
||||
self.env['pos_preparation_display.display'].create({
|
||||
'name': 'Super Smart Kitchen Display',
|
||||
})
|
||||
# There is a bug when we go the Field Service app (without any demo data) and we
|
||||
# click on the Studio button. It seems the fake group generated containing one record
|
||||
# to be used in the KanbanEditorRenderer has groupByField to undefined
|
||||
# (I guess it is because there is no group by?) and we got an error at this line
|
||||
# because we assume groupByField is defined.
|
||||
if 'project.task' in self.env and 'is_fsm' in self.env['project.task']:
|
||||
self.env['project.task'].create({
|
||||
'name': 'Zizizbroken',
|
||||
'project_id': self.env.ref('industry_fsm.fsm_project').id,
|
||||
'user_ids': [(4, self.env.ref('base.user_admin').id)],
|
||||
'date_deadline': datetime.now() + relativedelta(hour=12),
|
||||
'planned_date_begin': datetime.now() + relativedelta(hour=10),
|
||||
})
|
||||
self.browser_js("/odoo", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere(undefined, true);", "odoo.isReady === true", login="admin", timeout=120, success_signal="clickbot test succeeded")
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestMenusDemoLight(HttpCaseWithUserDemo):
|
||||
allow_end_on_form = True
|
||||
|
||||
def test_01_click_apps_menus_as_demo(self):
|
||||
# Disable onboarding tours to remove warnings
|
||||
if 'tour_enabled' in self.env['res.users']._fields:
|
||||
self.user_demo.tour_enabled = False
|
||||
# If not enabled (like in demo data), landing on website dashboard will redirect to /
|
||||
# and make the test crash
|
||||
group_website_designer = self.env.ref('website.group_website_designer', raise_if_not_found=False)
|
||||
if group_website_designer:
|
||||
self.env.ref('base.group_user').write({"implied_ids": [(4, group_website_designer.id)]})
|
||||
self.browser_js("/web", "odoo.__DEBUG__.services['web.clickEverywhere'](undefined, true);", "odoo.isReady === true", login="demo", timeout=120)
|
||||
self.browser_js("/odoo", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere(undefined, true);", "odoo.isReady === true", login="demo", timeout=120, success_signal="clickbot test succeeded")
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import re
|
||||
import secrets
|
||||
from io import BytesIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
|
||||
import odoo
|
||||
from odoo.modules.registry import Registry
|
||||
from odoo.tests.common import BaseCase, HttpCase, tagged
|
||||
from odoo.tools import config
|
||||
|
||||
|
|
@ -68,6 +71,14 @@ class TestDatabaseOperations(BaseCase):
|
|||
def assertDbs(self, dbs):
|
||||
self.assertEqual(self.list_dbs_filtered() - self.base_databases, set(dbs))
|
||||
|
||||
def url_open_drop(self, dbname):
|
||||
res = self.session.post(self.url('/web/database/drop'), data={
|
||||
'master_pwd': self.password,
|
||||
'name': dbname,
|
||||
}, allow_redirects=False)
|
||||
res.raise_for_status()
|
||||
return res
|
||||
|
||||
def test_database_creation(self):
|
||||
# check verify_admin_password patch
|
||||
self.assertTrue(odoo.tools.config.verify_admin_password(self.password))
|
||||
|
|
@ -84,14 +95,11 @@ class TestDatabaseOperations(BaseCase):
|
|||
'phone': '',
|
||||
}, allow_redirects=False)
|
||||
self.assertEqual(res.status_code, 303)
|
||||
self.assertIn('/web', res.headers['Location'])
|
||||
self.assertIn('/odoo', res.headers['Location'])
|
||||
self.assertDbs([test_db_name])
|
||||
|
||||
# delete the created database
|
||||
res = self.session.post(self.url('/web/database/drop'), data={
|
||||
'master_pwd': self.password,
|
||||
'name': test_db_name,
|
||||
}, allow_redirects=False)
|
||||
res = self.url_open_drop(test_db_name)
|
||||
self.assertEqual(res.status_code, 303)
|
||||
self.assertIn('/web/database/manager', res.headers['Location'])
|
||||
self.assertDbs([])
|
||||
|
|
@ -110,10 +118,163 @@ class TestDatabaseOperations(BaseCase):
|
|||
self.assertDbs([test_db_name])
|
||||
|
||||
# delete the created database
|
||||
res = self.session.post(self.url('/web/database/drop'), data={
|
||||
'master_pwd': self.password,
|
||||
'name': test_db_name,
|
||||
}, allow_redirects=False)
|
||||
self.assertEqual(res.status_code, 303)
|
||||
res = self.url_open_drop(test_db_name)
|
||||
self.assertIn('/web/database/manager', res.headers['Location'])
|
||||
self.assertDbs([])
|
||||
|
||||
def test_database_restore(self):
|
||||
test_db_name = self.db_name + '-test-database-restore'
|
||||
self.assertNotIn(test_db_name, self.list_dbs_filtered())
|
||||
|
||||
# backup the current database inside a temporary zip file
|
||||
res = self.session.post(
|
||||
self.url('/web/database/backup'),
|
||||
data={
|
||||
'master_pwd': self.password,
|
||||
'name': self.db_name,
|
||||
},
|
||||
allow_redirects=False,
|
||||
stream=True,
|
||||
)
|
||||
res.raise_for_status()
|
||||
datetime_pattern = r'\d\d\d\d-\d\d-\d\d_\d\d-\d\d-\d\d'
|
||||
self.assertRegex(
|
||||
res.headers.get('Content-Disposition'),
|
||||
fr"attachment; filename\*=UTF-8''{self.db_name}_{datetime_pattern}\.zip"
|
||||
)
|
||||
backup_file = BytesIO()
|
||||
backup_file.write(res.content)
|
||||
self.assertGreater(backup_file.tell(), 0, "The backup seems corrupted")
|
||||
|
||||
# upload the backup under a new name (create a duplicate)
|
||||
with self.subTest(DEFAULT_MAX_CONTENT_LENGTH=None), \
|
||||
patch.object(odoo.http, 'DEFAULT_MAX_CONTENT_LENGTH', None):
|
||||
backup_file.seek(0)
|
||||
self.session.post(
|
||||
self.url('/web/database/restore'),
|
||||
data={
|
||||
'master_pwd': self.password,
|
||||
'name': test_db_name,
|
||||
'copy': True,
|
||||
},
|
||||
files={
|
||||
'backup_file': backup_file,
|
||||
},
|
||||
allow_redirects=False
|
||||
).raise_for_status()
|
||||
self.assertDbs([test_db_name])
|
||||
self.url_open_drop(test_db_name)
|
||||
|
||||
# upload the backup again, this time simulating that the file is
|
||||
# too large under the default size limit, the default size limit
|
||||
# shouldn't apply to /web/database URLs
|
||||
with self.subTest(DEFAULT_MAX_CONTENT_LENGTH=1024), \
|
||||
patch.object(odoo.http, 'DEFAULT_MAX_CONTENT_LENGTH', 1024):
|
||||
backup_file.seek(0)
|
||||
self.session.post(
|
||||
self.url('/web/database/restore'),
|
||||
data={
|
||||
'master_pwd': self.password,
|
||||
'name': test_db_name,
|
||||
'copy': True,
|
||||
},
|
||||
files={
|
||||
'backup_file': backup_file,
|
||||
},
|
||||
allow_redirects=False
|
||||
).raise_for_status()
|
||||
self.assertDbs([test_db_name])
|
||||
self.url_open_drop(test_db_name)
|
||||
|
||||
|
||||
def test_database_http_registries(self):
|
||||
# This test is about dropping a connection inside one worker and
|
||||
# make sure that the other workers behave correctly.
|
||||
|
||||
#
|
||||
# Setup
|
||||
#
|
||||
|
||||
# duplicate this database
|
||||
test_db_name = self.db_name + '-test-database-duplicate'
|
||||
res = self.session.post(self.url('/web/database/duplicate'), data={
|
||||
'master_pwd': self.password,
|
||||
'name': self.db_name,
|
||||
'new_name': test_db_name,
|
||||
}, allow_redirects=False)
|
||||
|
||||
# get a registry and a cursor on that new database
|
||||
registry = Registry(test_db_name)
|
||||
cr = registry.cursor()
|
||||
self.assertIn(test_db_name, Registry.registries)
|
||||
|
||||
# delete the created database but keep the cursor
|
||||
with patch('odoo.sql_db.close_db') as close_db:
|
||||
res = self.url_open_drop(test_db_name)
|
||||
close_db.assert_called_once_with(test_db_name)
|
||||
|
||||
# simulate that some customers were connected to that dropped db
|
||||
session_store = odoo.http.root.session_store
|
||||
session = session_store.new()
|
||||
session.update(odoo.http.get_default_session(), db=test_db_name)
|
||||
session.context['lang'] = odoo.http.DEFAULT_LANG
|
||||
self.session.cookies['session_id'] = session.sid
|
||||
|
||||
# make it possible to inject the registry back
|
||||
patcher = patch.dict(Registry.registries.d, {test_db_name: registry})
|
||||
registries = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
#
|
||||
# Tests
|
||||
#
|
||||
|
||||
# The other worker doesn't have a registry in its LRU cache for
|
||||
# that session database.
|
||||
with self.subTest(msg="Registry.init() fails"):
|
||||
session_store.save(session)
|
||||
registries.pop('test_db_name', None)
|
||||
with self.assertLogs('odoo.sql_db', logging.INFO) as capture:
|
||||
res = self.session.get(self.url('/web/health'))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(session_store.get(session.sid)['db'], None)
|
||||
self.assertEqual(capture.output, [
|
||||
"INFO:odoo.sql_db:Connection to the database failed",
|
||||
])
|
||||
|
||||
|
||||
# The other worker has a registry in its LRU cache for that
|
||||
# session database. But it doesn't have a connection to the sql
|
||||
# database.
|
||||
with self.subTest(msg="Registry.cursor() fails"):
|
||||
session_store.save(session)
|
||||
registries[test_db_name] = registry
|
||||
with self.assertLogs('odoo.sql_db', logging.INFO) as capture, \
|
||||
patch.object(Registry, '__new__', return_value=registry):
|
||||
res = self.session.get(self.url('/web/health'))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(session_store.get(session.sid)['db'], None)
|
||||
self.assertEqual(capture.output, [
|
||||
"INFO:odoo.sql_db:Connection to the database failed",
|
||||
])
|
||||
|
||||
# The other worker has a registry in its LRU cache for that
|
||||
# session database. It also has a (now broken) connection to the
|
||||
# sql database.
|
||||
with self.subTest(msg="Registry.check_signaling() fails"):
|
||||
session_store.save(session)
|
||||
registries[test_db_name] = registry
|
||||
with self.assertLogs('odoo.sql_db', logging.ERROR) as capture, \
|
||||
patch.object(Registry, '__new__', return_value=registry), \
|
||||
patch.object(Registry, 'cursor', return_value=cr):
|
||||
res = self.session.get(self.url('/web/health'))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertEqual(session_store.get(session.sid)['db'], None)
|
||||
self.maxDiff = None
|
||||
self.assertRegex(capture.output[0], (
|
||||
r"^ERROR:odoo\.sql_db:bad query:(?s:.*?)"
|
||||
r"ERROR: terminating connection due to administrator command\s+"
|
||||
r"server closed the connection unexpectedly\s+"
|
||||
r"This probably means the server terminated abnormally\s+"
|
||||
r"before or while processing the request\.$"
|
||||
))
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@
|
|||
import io
|
||||
import base64
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from PIL import Image
|
||||
from werkzeug.urls import url_unquote_plus
|
||||
|
||||
from odoo.tools.misc import limited_field_access_token
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
|
||||
|
||||
|
|
@ -157,3 +160,77 @@ class TestImage(HttpCase):
|
|||
assert_filenames(f'/web/image/{att.id}/4wzb_!!63148-0-t1.jpg_360x1Q75.jpg_.webp',
|
||||
r"""4wzb_!!63148-0-t1.jpg_360x1Q75.jpg_.webp""",
|
||||
)
|
||||
|
||||
def test_05_web_image_access_token(self):
|
||||
"""Tests that valid access tokens grant access to binary data."""
|
||||
|
||||
def get_datetime_from_token(token):
|
||||
return datetime.fromtimestamp(int(token.rsplit("o", 1)[1], 16))
|
||||
|
||||
def get_datetime_from_record_field(record, field):
|
||||
return get_datetime_from_token(limited_field_access_token(record, field))
|
||||
|
||||
attachment = self.env["ir.attachment"].create(
|
||||
{
|
||||
"datas": b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
||||
"name": "test.gif",
|
||||
"mimetype": "image/gif",
|
||||
}
|
||||
)
|
||||
# no token: ko
|
||||
res = self.url_open(f"/web/image/{attachment.id}")
|
||||
res.raise_for_status()
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
# invalid token: ko
|
||||
res = self.url_open(f"/web/image/{attachment.id}?access_token=invalid_token")
|
||||
res.raise_for_status()
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
# valid token: ok
|
||||
token = limited_field_access_token(attachment, "raw")
|
||||
res = self.url_open(f"/web/image/{attachment.id}?access_token={token}")
|
||||
res.raise_for_status()
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=test.gif")
|
||||
# token about to expire: ok
|
||||
with freeze_time(get_datetime_from_token(token) - timedelta(seconds=1)):
|
||||
res = self.url_open(f"/web/image/{attachment.id}?access_token={token}")
|
||||
res.raise_for_status()
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=test.gif")
|
||||
# expired token: ko
|
||||
with freeze_time(get_datetime_from_token(token)):
|
||||
res = self.url_open(f"/web/image/{attachment.id}?access_token={token}")
|
||||
res.raise_for_status()
|
||||
self.assertEqual(res.headers["Content-Disposition"], "inline; filename=placeholder.png")
|
||||
# within a 14-days period, the same token is generated
|
||||
start_of_period = datetime(2021, 2, 18, 0, 0, 0) # 14-days period 2021-02-18 to 2021-03-04
|
||||
base_result = datetime(2021, 3, 24, 15, 25, 40)
|
||||
for i in range(14):
|
||||
with freeze_time(start_of_period + timedelta(days=i, hours=i % 24, minutes=i % 60)):
|
||||
self.assertEqual(
|
||||
get_datetime_from_record_field(self.env["ir.attachment"].browse(2), "raw"),
|
||||
base_result,
|
||||
)
|
||||
# on each following 14-days period another token is generated, valid for exactly 14 extra
|
||||
# days from the previous token
|
||||
for i in range(50):
|
||||
with freeze_time(
|
||||
start_of_period + timedelta(days=14 * i + i % 14, hours=i % 24, minutes=i % 60)
|
||||
):
|
||||
self.assertEqual(
|
||||
get_datetime_from_record_field(self.env["ir.attachment"].browse(2), "raw"),
|
||||
base_result + timedelta(days=14 * i),
|
||||
)
|
||||
with freeze_time(datetime(2021, 3, 1, 1, 2, 3)):
|
||||
# at the same time...
|
||||
self.assertEqual(
|
||||
get_datetime_from_record_field(self.env["ir.attachment"].browse(2), "raw"),
|
||||
base_result,
|
||||
)
|
||||
# a different record generates a different token
|
||||
record_res = get_datetime_from_record_field(self.env["ir.attachment"].browse(3), "raw")
|
||||
self.assertNotIn(record_res, [base_result])
|
||||
# a different field generates a different token
|
||||
field_res = get_datetime_from_record_field(self.env["ir.attachment"].browse(3), "datas")
|
||||
self.assertNotIn(field_res, [base_result, record_res])
|
||||
# a different model generates a different token
|
||||
model_res = get_datetime_from_record_field(self.env["res.partner"].browse(3), "raw")
|
||||
self.assertNotIn(model_res, [base_result, record_res, field_res])
|
||||
|
|
|
|||
|
|
@ -63,3 +63,12 @@ class IrModelAccessTest(TransactionCase):
|
|||
# non existent model comes after existent model
|
||||
result = self.env['ir.model'].display_name_for(["res.company", "unexistent"])
|
||||
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}, {"display_name": "unexistent", "model": "unexistent"}])
|
||||
# transient models
|
||||
result = self.env['ir.model'].display_name_for(["res.company", "base.language.export"])
|
||||
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}, {"display_name": "base.language.export", "model": "base.language.export"}])
|
||||
|
||||
# do not return results for transient models
|
||||
result = self.env['ir.model'].get_available_models()
|
||||
result = {values["model"] for values in result}
|
||||
self.assertIn("res.company", result)
|
||||
self.assertNotIn("base.language.export", result)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import base64
|
||||
from lxml import etree
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
|
||||
class TestIrQweb(TransactionCase):
|
||||
def test_image_field(self):
|
||||
|
|
@ -28,3 +31,72 @@ class TestIrQweb(TransactionCase):
|
|||
self.assertTrue(img.get("src").startswith("/web/image"))
|
||||
self.assertEqual(img.get("class"), "img img-fluid")
|
||||
self.assertEqual(img.get("alt"), "test image partner")
|
||||
|
||||
def test_image_field_webp(self):
|
||||
webp = "UklGRsCpAQBXRUJQVlA4WAoAAAAQAAAAGAQA/wMAQUxQSMywAAAdNANp22T779/0RUREkvqLOTPesG1T21jatpLTSbpXQzTMEw3zWMM81jCPnWG2fTM7vpndvpkd38y2758Y+6a/Ld/Mt3zzT/XwzCKlV0Ooo61UpZIsKLjKc98R"
|
||||
webp_decoded = base64.b64decode(webp)
|
||||
self.assertEqual(guess_mimetype(webp_decoded), "image/webp")
|
||||
|
||||
view = self.env["ir.ui.view"].create({
|
||||
"key": "web.test_qweb",
|
||||
"type": "qweb",
|
||||
"arch": """<t t-name="test_qweb">
|
||||
<span t-field="record.flag_image" t-options-widget="'image'" t-options-qweb_img_raw_data="is_raw_image" />
|
||||
</t>"""
|
||||
})
|
||||
lang_record = self.env["res.lang"].create({
|
||||
"name": "test lang",
|
||||
"flag_image": webp,
|
||||
"code": "TEST"
|
||||
})
|
||||
attachment = self.env["ir.attachment"].search([
|
||||
("res_model", "=", "res.lang"),
|
||||
("res_id", '=', lang_record.id),
|
||||
("res_field", "=", "flag_image")
|
||||
])
|
||||
|
||||
jpeg_attach = self.env["ir.attachment"].create({
|
||||
"name": "webpcopy.jpg",
|
||||
"res_model": "ir.attachment",
|
||||
"res_id": attachment.id,
|
||||
"datas": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVR4nGJxKFrEwMDAxAAGgAAAAP//D+IBWx9K7TUAAAAASUVORK5CYII="
|
||||
})
|
||||
jpeg_datas = jpeg_attach.datas
|
||||
|
||||
html = view.with_context(webp_as_jpg=False)._render_template(view.id, {"is_raw_image": True, "record": lang_record})
|
||||
tree = etree.fromstring(html)
|
||||
img = tree.find("img")
|
||||
self.assertEqual(img.get("src"), "data:image/webp;base64,%s" % webp)
|
||||
|
||||
html = view.with_context(webp_as_jpg=True)._render_template(view.id, {"is_raw_image": True, "record": lang_record})
|
||||
tree = etree.fromstring(html)
|
||||
img = tree.find("img")
|
||||
self.assertEqual(img.get("src"), "data:image/png;base64,%s" % jpeg_datas.decode())
|
||||
|
||||
def test_image_svg(self):
|
||||
image = """<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<svg height='180' width='180' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
|
||||
<rect fill='#ff0000' height='180' width='180'/>
|
||||
<text fill='#ffffff' font-size='96' text-anchor='middle' x='90' y='125' font-family='sans-serif'>H</text>
|
||||
</svg>"""
|
||||
|
||||
b64_image = base64.b64encode(image.encode()).decode()
|
||||
view = self.env["ir.ui.view"].create({
|
||||
"key": "web.test_qweb",
|
||||
"type": "qweb",
|
||||
"arch": """<t t-name="test_qweb">
|
||||
<span t-field="record.flag_image" t-options-widget="'image'" t-options-qweb_img_raw_data="True" />
|
||||
</t>"""
|
||||
})
|
||||
partner = self.env["res.lang"].create({
|
||||
"name": "test image partner",
|
||||
"flag_image": b64_image,
|
||||
"code": "TEST"
|
||||
})
|
||||
|
||||
html = view._render_template(view.id, {"record": partner})
|
||||
tree = etree.fromstring(html)
|
||||
img = tree.find("img")
|
||||
self.assertEqual(img.get("src"), f"data:image/svg+xml;base64,{b64_image}")
|
||||
self.assertEqual(img.get("class"), "img img-fluid")
|
||||
self.assertEqual(img.get("alt"), "test image partner")
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
from contextlib import suppress
|
||||
|
||||
import odoo.tests
|
||||
from odoo.tools.misc import file_open
|
||||
from werkzeug.urls import url_quote_plus
|
||||
|
||||
RE_FORBIDDEN_STATEMENTS = re.compile(r'test.*\.(only|debug)\(')
|
||||
RE_ONLY = re.compile(r'QUnit\.(only|debug)\(')
|
||||
|
||||
|
||||
def unit_test_error_checker(message):
|
||||
return '[HOOT]' not in message
|
||||
|
||||
|
||||
def qunit_error_checker(message):
|
||||
# ! DEPRECATED
|
||||
# We don't want to stop qunit if a qunit is breaking.
|
||||
|
||||
# '%s/%s test failed.' case: end message when all tests are finished
|
||||
|
|
@ -22,47 +31,62 @@ def qunit_error_checker(message):
|
|||
return True # in other cases, always stop (missing dependency, ...)
|
||||
|
||||
|
||||
def _get_filters(test_params):
|
||||
filters = []
|
||||
for sign, param in test_params:
|
||||
parts = param.split(',')
|
||||
for part in parts:
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
part_sign = sign
|
||||
if part.startswith('-'):
|
||||
part = part[1:]
|
||||
part_sign = '-' if sign == '+' else '+'
|
||||
filters.append((part_sign, part))
|
||||
return sorted(filters)
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class WebsuiteCommon(odoo.tests.HttpCase):
|
||||
def get_filter(self, test_params):
|
||||
positive = []
|
||||
negative = []
|
||||
for sign, param in test_params:
|
||||
filters = param.split(',')
|
||||
for filter in filters:
|
||||
filter = filter.strip()
|
||||
if not filter:
|
||||
continue
|
||||
negate = sign == '-'
|
||||
if filter.startswith('-'):
|
||||
negate = not negate
|
||||
filter = filter[1:]
|
||||
if negate:
|
||||
negative.append(f'({re.escape(filter)}.*)')
|
||||
else:
|
||||
positive.append(f'({re.escape(filter)}.*)')
|
||||
class QunitCommon(odoo.tests.HttpCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.qunit_filters = self.get_qunit_filters()
|
||||
|
||||
def get_qunit_regex(self, test_params):
|
||||
filters = _get_filters(test_params)
|
||||
positive = [f'({re.escape(f)}.*)' for sign, f in filters if sign == '+']
|
||||
negative = [f'({re.escape(f)}.*)' for sign, f in filters if sign == '-']
|
||||
filter = ''
|
||||
if positive or negative:
|
||||
if filters:
|
||||
positive_re = '|'.join(positive) or '.*'
|
||||
negative_re = '|'.join(negative)
|
||||
negative_re = f'(?!{negative_re})' if negative_re else ''
|
||||
filter = f'^({negative_re})({positive_re})$'
|
||||
return filter
|
||||
|
||||
def test_get_filter(self):
|
||||
f1 = self.get_filter([('+', 'utils,mail,-utils > bl1,-utils > bl2')])
|
||||
f2 = self.get_filter([('+', 'utils'), ('-', 'utils > bl1,utils > bl2'), ('+', 'mail')])
|
||||
for f in (f1, f2):
|
||||
self.assertRegex('utils', f)
|
||||
self.assertRegex('mail', f)
|
||||
self.assertRegex('utils > something', f)
|
||||
def get_qunit_filters(self):
|
||||
filter_param = ''
|
||||
filter = self.get_qunit_regex(self._test_params)
|
||||
if filter:
|
||||
url_filter = url_quote_plus(filter)
|
||||
filter_param = f'&filter=/{url_filter}/'
|
||||
return filter_param
|
||||
|
||||
self.assertNotRegex('utils > bl1', f)
|
||||
self.assertNotRegex('utils > bl2', f)
|
||||
self.assertNotRegex('web', f)
|
||||
def test_get_qunit_regex(self):
|
||||
f = self.get_qunit_regex([('+', 'utils,mail,-utils > bl1,-utils > bl2')])
|
||||
f2 = self.get_qunit_regex([('+', 'utils'), ('-', 'utils > bl1,utils > bl2'), ('+', 'mail')])
|
||||
self.assertEqual(f, f2)
|
||||
self.assertRegex('utils', f)
|
||||
self.assertRegex('mail', f)
|
||||
self.assertRegex('utils > something', f)
|
||||
|
||||
f2 = self.get_filter([('+', '-utils > bl1,-utils > bl2')])
|
||||
f3 = self.get_filter([('-', 'utils > bl1,utils > bl2')])
|
||||
self.assertNotRegex('utils > bl1', f)
|
||||
self.assertNotRegex('utils > bl2', f)
|
||||
self.assertNotRegex('web', f)
|
||||
|
||||
f2 = self.get_qunit_regex([('+', '-utils > bl1,-utils > bl2')])
|
||||
f3 = self.get_qunit_regex([('-', 'utils > bl1,utils > bl2')])
|
||||
for f in (f2, f3):
|
||||
self.assertRegex('utils', f)
|
||||
self.assertRegex('mail', f)
|
||||
|
|
@ -72,30 +96,93 @@ class WebsuiteCommon(odoo.tests.HttpCase):
|
|||
self.assertNotRegex('utils > bl1', f)
|
||||
self.assertNotRegex('utils > bl2', f)
|
||||
|
||||
def get_filter_param(self):
|
||||
filter_param = ''
|
||||
filter = self.get_filter(self._test_params)
|
||||
if filter:
|
||||
url_filter = url_quote_plus(filter)
|
||||
filter_param = f'&filter=/{url_filter}/'
|
||||
return filter_param
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class HOOTCommon(odoo.tests.HttpCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.hoot_filters = self.get_hoot_filters()
|
||||
|
||||
def _generate_hash(self, test_string):
|
||||
hash = 0
|
||||
for char in test_string:
|
||||
hash = (hash << 5) - hash + ord(char)
|
||||
hash = hash & 0xFFFFFFFF
|
||||
return f'{hash:08x}'
|
||||
|
||||
def get_hoot_filters(self):
|
||||
filters = _get_filters(self._test_params)
|
||||
filter = ''
|
||||
for sign, f in filters:
|
||||
h = self._generate_hash(f)
|
||||
if sign == '-':
|
||||
h = f'-{h}'
|
||||
# Since we don't know if the descriptor we have is a test or a suite, we need to provide the hash for a generic "job"
|
||||
filter += f'&id={h}'
|
||||
return filter
|
||||
|
||||
def test_generate_hoot_hash(self):
|
||||
self.assertEqual(self._generate_hash('@web/core'), 'e39ce9ba')
|
||||
self.assertEqual(self._generate_hash('@web/core/autocomplete'), '69a6561d') # suite
|
||||
self.assertEqual(self._generate_hash('@web/core/autocomplete/open dropdown on input'), 'ee565d54') # test
|
||||
|
||||
def test_get_hoot_filter(self):
|
||||
self._test_params = []
|
||||
self.assertEqual(self.get_hoot_filters(), '')
|
||||
expected = '&id=e39ce9ba&id=-69a6561d'
|
||||
self._test_params = [('+', '@web/core,-@web/core/autocomplete')]
|
||||
self.assertEqual(self.get_hoot_filters(), expected)
|
||||
self._test_params = [('+', '@web/core'), ('-', '@web/core/autocomplete')]
|
||||
self.assertEqual(self.get_hoot_filters(), expected)
|
||||
self._test_params = [('+', '-@web/core/autocomplete,-@web/core/autocomplete2')]
|
||||
self.assertEqual(self.get_hoot_filters(), '&id=-69a6561d&id=-cb246db5')
|
||||
self._test_params = [('-', '-@web/core/autocomplete,-@web/core/autocomplete2')]
|
||||
self.assertEqual(self.get_hoot_filters(), '&id=69a6561d&id=cb246db5')
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class WebSuite(WebsuiteCommon):
|
||||
class WebSuite(QunitCommon, HOOTCommon):
|
||||
|
||||
@odoo.tests.no_retry
|
||||
def test_js(self):
|
||||
filter_param = self.get_filter_param()
|
||||
# webclient desktop test suite
|
||||
self.browser_js('/web/tests?mod=web%s' % filter_param, "", "", login='admin', timeout=1800, error_checker=qunit_error_checker)
|
||||
def test_unit_desktop(self):
|
||||
# Unit tests suite (desktop)
|
||||
self.browser_js(f'/web/tests?headless&loglevel=2&preset=desktop&timeout=15000{self.hoot_filters}', "", "", login='admin', timeout=1800, success_signal="[HOOT] Test suite succeeded", error_checker=unit_test_error_checker)
|
||||
|
||||
@odoo.tests.no_retry
|
||||
def test_hoot(self):
|
||||
# HOOT tests suite
|
||||
self.browser_js(f'/web/static/lib/hoot/tests/index.html?headless&loglevel=2{self.hoot_filters}', "", "", login='admin', timeout=1800, success_signal="[HOOT] Test suite succeeded", error_checker=unit_test_error_checker)
|
||||
|
||||
@odoo.tests.no_retry
|
||||
def test_qunit_desktop(self):
|
||||
# ! DEPRECATED
|
||||
self.browser_js(f'/web/tests/legacy?mod=web{self.qunit_filters}', "", "", login='admin', timeout=1800, success_signal="QUnit test suite done.", error_checker=qunit_error_checker)
|
||||
|
||||
def test_check_suite(self):
|
||||
# verify no js test is using `QUnit.only` as it forbid any other test to be executed
|
||||
self._check_forbidden_statements('web.assets_unit_tests')
|
||||
# Checks that no test is using `only` or `debug` as it prevents other tests to be run
|
||||
self._check_only_call('web.qunit_suite_tests')
|
||||
self._check_only_call('web.qunit_mobile_suite_tests')
|
||||
|
||||
def _check_forbidden_statements(self, bundle):
|
||||
# As we currently are not in a request context, we cannot render `web.layout`.
|
||||
# We then re-define 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-esc="head"/></head></t>'})
|
||||
|
||||
assets = self.env['ir.qweb']._get_asset_content(bundle)[0]
|
||||
if len(assets) == 0:
|
||||
self.fail("No assets found in the given test bundle")
|
||||
|
||||
for asset in assets:
|
||||
filename = asset['filename']
|
||||
if not filename.endswith('.test.js'):
|
||||
continue
|
||||
with suppress(FileNotFoundError):
|
||||
with file_open(filename, 'rb', filter_ext=('.js',)) as fp:
|
||||
if RE_FORBIDDEN_STATEMENTS.search(fp.read().decode('utf-8')):
|
||||
self.fail("`only()` or `debug()` used in file %r" % asset['url'])
|
||||
|
||||
def _check_only_call(self, suite):
|
||||
# ! DEPRECATED
|
||||
# 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-esc="head"/></head></t>'})
|
||||
|
|
@ -106,19 +193,24 @@ class WebSuite(WebsuiteCommon):
|
|||
|
||||
for asset in assets:
|
||||
filename = asset['filename']
|
||||
if not filename or asset['atype'] != 'text/javascript':
|
||||
if not filename.endswith('.js'):
|
||||
continue
|
||||
with open(filename, 'rb') as fp:
|
||||
if RE_ONLY.search(fp.read().decode('utf-8')):
|
||||
self.fail("`QUnit.only()` or `QUnit.debug()` used in file %r" % asset['url'])
|
||||
with suppress(FileNotFoundError):
|
||||
with file_open(filename, 'rb', filter_ext=('.js',)) as fp:
|
||||
if RE_ONLY.search(fp.read().decode('utf-8')):
|
||||
self.fail("`QUnit.only()` or `QUnit.debug()` used in file %r" % asset['url'])
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class MobileWebSuite(WebsuiteCommon):
|
||||
class MobileWebSuite(QunitCommon, HOOTCommon):
|
||||
browser_size = '375x667'
|
||||
touch_enabled = True
|
||||
|
||||
def test_mobile_js(self):
|
||||
filter_param = self.get_filter_param()
|
||||
# webclient mobile test suite
|
||||
self.browser_js('/web/tests/mobile?mod=web%s' % filter_param, "", "", login='admin', timeout=1800, error_checker=qunit_error_checker)
|
||||
@odoo.tests.no_retry
|
||||
def test_unit_mobile(self):
|
||||
# Unit tests suite (mobile)
|
||||
self.browser_js(f'/web/tests?headless&loglevel=2&preset=mobile&tag=-headless&timeout=15000{self.hoot_filters}', "", "", login='admin', timeout=1800, success_signal="[HOOT] Test suite succeeded", error_checker=unit_test_error_checker)
|
||||
|
||||
def test_qunit_mobile(self):
|
||||
# ! DEPRECATED
|
||||
self.browser_js(f'/web/tests/legacy/mobile?mod=web{self.qunit_filters}', "", "", login='admin', timeout=1800, success_signal="QUnit test suite done.", error_checker=qunit_error_checker)
|
||||
|
|
|
|||
|
|
@ -17,22 +17,24 @@ class LoadMenusTests(HttpCase):
|
|||
|
||||
def test_load_menus(self):
|
||||
menu_loaded = self.url_open("/web/webclient/load_menus/1234")
|
||||
|
||||
expected = {
|
||||
str(self.menu.id): {
|
||||
"actionID": False,
|
||||
"actionModel": False,
|
||||
"actionPath": False,
|
||||
"appID": self.menu.id,
|
||||
"children": [],
|
||||
"id": self.menu.id,
|
||||
"name": "test_menu",
|
||||
"webIcon": False,
|
||||
"webIconData": False,
|
||||
"webIconData": "/web/static/img/default_icon_app.png",
|
||||
"webIconDataMimetype": False,
|
||||
"xmlid": ""
|
||||
},
|
||||
"root": {
|
||||
"actionID": False,
|
||||
"actionModel": False,
|
||||
"actionPath": False,
|
||||
"appID": False,
|
||||
"children": [
|
||||
self.menu.id,
|
||||
|
|
@ -41,6 +43,7 @@ class LoadMenusTests(HttpCase):
|
|||
"name": "root",
|
||||
"webIcon": None,
|
||||
"webIconData": None,
|
||||
"webIconDataMimetype": None,
|
||||
"xmlid": "",
|
||||
"backgroundImage": None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.tests.common import get_db_name, HOST, HttpCase, new_test_user, Opener
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.tests.common import get_db_name, HOST, HttpCase, new_test_user, Opener, tagged
|
||||
|
||||
|
||||
class TestWebLoginCommon(HttpCase):
|
||||
|
|
@ -40,7 +41,7 @@ class TestWebLogin(TestWebLoginCommon):
|
|||
data='{}'
|
||||
).raise_for_status()
|
||||
# ensure we end up on the right page for internal users.
|
||||
self.assertEqual(res_post.request.path_url, '/web')
|
||||
self.assertEqual(res_post.request.path_url, '/odoo')
|
||||
|
||||
def test_web_login_external(self):
|
||||
res_post = self.login('portal_user', 'portal_user')
|
||||
|
|
@ -58,3 +59,9 @@ class TestWebLogin(TestWebLoginCommon):
|
|||
|
||||
# log in using the above form, it should still be valid
|
||||
self.login('internal_user', 'internal_user', csrf_token)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestUserSwitch(HttpCaseWithUserDemo):
|
||||
def test_user_switch(self):
|
||||
self.start_tour('/odoo', 'test_user_switch', login='demo')
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.addons.web.controllers.utils import fix_view_modes
|
||||
|
||||
|
||||
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']
|
||||
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"
|
||||
}
|
||||
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"
|
||||
}
|
||||
fix_view_modes(action)
|
||||
|
||||
self.assertEqual(action, {
|
||||
"views": [[False, "list"], [False, "form"],
|
||||
[False, "calendar"], [42, "list"]],
|
||||
"view_id": False,
|
||||
"view_mode": "list,form,calendar"
|
||||
})
|
||||
|
|
@ -67,7 +67,7 @@ class TestProfilingWeb(ProfilingHttpCase):
|
|||
descriptions = self.env['ir.profile'].search([], order='id desc', limit=3).mapped('name')
|
||||
self.assertEqual(descriptions, [
|
||||
f'test_profile_test_tool uid:{self.env.uid} warm ',
|
||||
f'test_profile_test_tool uid:{self.env.uid} warm /web/login?',
|
||||
f'test_profile_test_tool uid:{self.env.uid} warm /web/login?redirect=%2Fweb%3F',
|
||||
f'test_profile_test_tool uid:{self.env.uid} warm /web?',
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,158 +0,0 @@
|
|||
# -*- 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_read_progress_bar_m2m(self):
|
||||
""" Test that read_progress_bar works with m2m field grouping """
|
||||
progressbar = {
|
||||
'field': 'type',
|
||||
'colors': {
|
||||
'contact': 'success', 'private': 'danger', 'other': '200',
|
||||
}
|
||||
}
|
||||
result = self.env['res.partner'].read_progress_bar([], 'category_id', progressbar)
|
||||
# check that it works when grouping by m2m field
|
||||
self.assertTrue(result)
|
||||
# check the null group
|
||||
self.assertIn('False', result)
|
||||
|
||||
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},
|
||||
})
|
||||
|
|
@ -37,23 +37,27 @@ class TestReports(odoo.tests.HttpCase):
|
|||
result = {}
|
||||
origin_find_record = self.env.registry['ir.binary']._find_record
|
||||
|
||||
def _find_record(self, xmlid=None, res_model='ir.attachment', res_id=None, access_token=None):
|
||||
def _find_record(self, xmlid=None, res_model='ir.attachment', res_id=None, access_token=None, field=None):
|
||||
if res_model == 'ir.attachment' and res_id == image.id:
|
||||
result['uid'] = self.env.uid
|
||||
record = origin_find_record(self, xmlid, res_model, res_id, access_token)
|
||||
record = origin_find_record(self, xmlid, res_model, res_id, access_token, field)
|
||||
result.update({'record_id': record.id, 'data': record.datas})
|
||||
else:
|
||||
record = origin_find_record(self, xmlid, res_model, res_id, access_token)
|
||||
record = origin_find_record(self, xmlid, res_model, res_id, access_token, field)
|
||||
return record
|
||||
|
||||
self.patch(self.env.registry['ir.binary'], '_find_record', _find_record)
|
||||
|
||||
# 1. Request the report as admin, who has access to the image
|
||||
admin = self.env.ref('base.user_admin')
|
||||
admin_device_log_count_before = self.env['res.device.log'].search_count([('user_id', '=', admin.id)])
|
||||
report = report.with_user(admin)
|
||||
with MockRequest(report.env) as mock_request:
|
||||
mock_request.session.sid = self.authenticate(admin.login, admin.login).sid
|
||||
mock_request.session = self.authenticate(admin.login, admin.login)
|
||||
report.with_context(force_report_rendering=True)._render_qweb_pdf(report.id, [partner_id])
|
||||
# Check that no device logs have been generated
|
||||
admin_device_log_count_after = self.env['res.device.log'].search_count([('user_id', '=', admin.id)])
|
||||
self.assertFalse(admin_device_log_count_after - admin_device_log_count_before)
|
||||
|
||||
self.assertEqual(
|
||||
result.get('uid'), admin.id, 'wkhtmltopdf is not fetching the image as the user printing the report'
|
||||
|
|
@ -65,9 +69,14 @@ class TestReports(odoo.tests.HttpCase):
|
|||
self.logout()
|
||||
result.clear()
|
||||
public = self.env.ref('base.public_user')
|
||||
public_device_log_count_before = self.env['res.device.log'].search_count([('user_id', '=', public.id)])
|
||||
report = report.with_user(public)
|
||||
with MockRequest(self.env) as mock_request:
|
||||
mock_request.session = self.authenticate(None, None)
|
||||
report.with_context(force_report_rendering=True)._render_qweb_pdf(report.id, [partner_id])
|
||||
# Check that no device logs have been generated
|
||||
public_device_log_count_after = self.env['res.device.log'].search_count([('user_id', '=', public.id)])
|
||||
self.assertFalse(public_device_log_count_after - public_device_log_count_before)
|
||||
|
||||
self.assertEqual(
|
||||
result.get('uid'), public.id, 'wkhtmltopdf is not fetching the image as the user printing the report'
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ class TestSessionInfo(common.HttpCase):
|
|||
cls.company_a = cls.env['res.company'].create({'name': "A"})
|
||||
cls.company_b = cls.env['res.company'].create({'name': "B"})
|
||||
cls.company_c = cls.env['res.company'].create({'name': "C"})
|
||||
cls.companies = [cls.company_a, cls.company_b, cls.company_c]
|
||||
cls.company_b_branch = cls.env['res.company'].create({'name': "B Branch", 'parent_id': cls.company_b.id})
|
||||
cls.company_c_branch = cls.env['res.company'].create({'name': "C Branch", 'parent_id': cls.company_c.id})
|
||||
cls.company_c_branch_branch = cls.env['res.company'].create({'name': "C Branch Branch", 'parent_id': cls.company_c_branch.id})
|
||||
cls.allowed_companies = cls.company_a + cls.company_b_branch + cls.company_c + cls.company_c_branch_branch
|
||||
cls.disallowed_ancestor_companies = cls.company_b + cls.company_c_branch
|
||||
|
||||
cls.user_password = "info"
|
||||
cls.user = common.new_test_user(
|
||||
|
|
@ -25,7 +29,7 @@ class TestSessionInfo(common.HttpCase):
|
|||
tz="UTC")
|
||||
cls.user.write({
|
||||
'company_id': cls.company_a.id,
|
||||
'company_ids': [Command.set([company.id for company in cls.companies])],
|
||||
'company_ids': [Command.set(cls.allowed_companies.ids)],
|
||||
})
|
||||
|
||||
cls.payload = json.dumps(dict(jsonrpc="2.0", method="call", id=str(uuid4())))
|
||||
|
|
@ -36,6 +40,9 @@ class TestSessionInfo(common.HttpCase):
|
|||
def test_session_info(self):
|
||||
""" Checks that the session_info['user_companies'] structure correspond to what is expected """
|
||||
self.authenticate(self.user.login, self.user_password)
|
||||
# Avoid the create part of res.users.settings since get_session_info
|
||||
# route is readonly
|
||||
self.env['res.users.settings']._find_or_create_for_user(self.user)
|
||||
response = self.url_open("/web/session/get_session_info", data=self.payload, headers=self.headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
|
@ -47,11 +54,25 @@ class TestSessionInfo(common.HttpCase):
|
|||
'id': company.id,
|
||||
'name': company.name,
|
||||
'sequence': company.sequence,
|
||||
} for company in self.companies
|
||||
'child_ids': company.child_ids.ids,
|
||||
'parent_id': company.parent_id.id,
|
||||
} for company in self.allowed_companies
|
||||
}
|
||||
|
||||
expected_disallowed_ancestor_companies = {
|
||||
str(company.id): {
|
||||
'id': company.id,
|
||||
'name': company.name,
|
||||
'sequence': company.sequence,
|
||||
'child_ids': company.child_ids.ids,
|
||||
'parent_id': company.parent_id.id,
|
||||
} for company in self.disallowed_ancestor_companies
|
||||
}
|
||||
|
||||
expected_user_companies = {
|
||||
'current_company': self.company_a.id,
|
||||
'allowed_companies': expected_allowed_companies,
|
||||
'disallowed_ancestor_companies': expected_disallowed_ancestor_companies,
|
||||
}
|
||||
self.assertEqual(
|
||||
result['user_companies'],
|
||||
|
|
@ -63,12 +84,3 @@ class TestSessionInfo(common.HttpCase):
|
|||
response = self.url_open("/web/session/modules", data=self.payload, headers=self.headers)
|
||||
data = response.json()
|
||||
self.assertTrue(isinstance(data['result'], list))
|
||||
|
||||
def test_load_polish_lang(self):
|
||||
# Regression test, making sure languages without thousand separators
|
||||
# work correctly
|
||||
lang_pl = self.env['res.lang']._activate_lang('pl_PL')
|
||||
self.user.lang = lang_pl.code
|
||||
self.authenticate(self.user.login, self.user_password)
|
||||
res = self.url_open('/web')
|
||||
res.raise_for_status()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ class TestWebSearchRead(common.TransactionCase):
|
|||
cls.ResCurrency = cls.env['res.currency'].with_context(active_test=False)
|
||||
cls.max = cls.ResCurrency.search_count([])
|
||||
|
||||
def assert_web_search_read(self, expected_length, expected_records_length, expected_search_count_called=True, **kwargs):
|
||||
def assert_web_search_read(self, expected_length, expected_records_length, expected_search_count_called=True,
|
||||
**kwargs):
|
||||
original_search_count = self.ResCurrency.search_count
|
||||
search_count_called = [False]
|
||||
|
||||
|
|
@ -21,13 +22,13 @@ class TestWebSearchRead(common.TransactionCase):
|
|||
return original_search_count(*method_args, **method_kwargs)
|
||||
|
||||
with patch('odoo.addons.base.models.res_currency.Currency.search_count', new=search_count):
|
||||
results = self.ResCurrency.web_search_read(domain=[], fields=['id'], **kwargs)
|
||||
results = self.ResCurrency.web_search_read(domain=[], specification={'id':{}}, **kwargs)
|
||||
|
||||
self.assertEqual(results['length'], expected_length)
|
||||
self.assertEqual(len(results['records']), expected_records_length)
|
||||
self.assertEqual(search_count_called[0], expected_search_count_called)
|
||||
|
||||
def test_web_search_read(self):
|
||||
def test_unity_web_search_read(self):
|
||||
self.assert_web_search_read(self.max, self.max, expected_search_count_called=False)
|
||||
self.assert_web_search_read(self.max, 2, limit=2)
|
||||
self.assert_web_search_read(self.max, 2, limit=2, offset=10)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue