Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,21 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_db_manager
from . import test_health
from . import test_image
from . import test_ir_model
from . import test_js
from . import test_menu
from . import test_click_everywhere
from . import test_base_document_layout
from . import test_load_menus
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_domain
from . import test_ir_qweb
from . import test_reports

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,86 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import time
import odoo
import odoo.tests
from odoo.tests.common import HttpCase
from odoo.modules.module import get_manifest
from odoo.tools import mute_logger
from unittest.mock import patch
_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
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 bundle 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)
except ValueError:
_logger.info('Error detected while generating bundle %r %s', bundle, assets_type)
@odoo.tests.tagged('post_install', '-at_install', 'assets_bundle')
class TestLogsAssetsGenerateTime(TestAssetsGenerateTimeCommon):
def test_logs_assets_generate_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.
"""
for bundle, duration in self.generate_bundles():
_logger.info('Bundle %r generated in %.2fs', bundle, duration)
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'assets_bundle')
class TestAssetsGenerateTime(TestAssetsGenerateTimeCommon):
"""
This test is meant to be run nightly to ensure bundle generation does not exceed
a low threshold
"""
def test_assets_generate_time(self):
thresholds = {
'web.qunit_suite_tests.js': 3.6,
'project.webclient.js': 2.5,
'point_of_sale.pos_assets_backend.js': 2.5,
'web.assets_backend.js': 2.5,
}
for bundle, duration in self.generate_bundles():
threshold = thresholds.get(bundle, 2)
self.assertLess(duration, threshold, "Bundle %r took more than %s sec" % (bundle, threshold))
@odoo.tests.tagged('post_install', '-at_install')
class TestLoad(HttpCase):
def test_assets_already_exists(self):
self.authenticate('admin', 'admin')
_save_attachment = odoo.addons.base.models.assetsbundle.AssetsBundle.save_attachment
def save_attachment(bundle, extension, content):
attachment = _save_attachment(bundle, extension, content)
message = f"Trying to save an attachement for {bundle.name} when it should already exist: {attachment.url}"
_logger.error(message)
return attachment
with patch('odoo.addons.base.models.assetsbundle.AssetsBundle.save_attachment', save_attachment):
self.url_open('/web').raise_for_status()
self.url_open('/').raise_for_status()

View file

@ -0,0 +1,933 @@
# 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)

View file

@ -0,0 +1,238 @@
import os
from PIL import Image
from functools import partial
from odoo.tests import TransactionCase, tagged, Form
from odoo.tools import frozendict, image_to_base64, hex_to_rgb
dir_path = os.path.dirname(os.path.realpath(__file__))
_file_cache = {}
class TestBaseDocumentLayoutHelpers(TransactionCase):
#
# Public
#
def setUp(self):
super(TestBaseDocumentLayoutHelpers, self).setUp()
self.color_fields = ['primary_color', 'secondary_color']
self.company = self.env.company
self.css_color_error = 0
self._set_templates_and_layouts()
self._set_images()
def assertColors(self, checked_obj, expected):
_expected_getter = expected.get if isinstance(expected, dict) else partial(getattr, expected)
for fname in self.color_fields:
color1 = getattr(checked_obj, fname)
color2 = _expected_getter(fname)
if self.css_color_error:
self._compare_colors_rgb(color1, color2)
else:
self.assertEqual(color1, color2)
#
# Private
#
def _compare_colors_rgb(self, color1, color2):
self.assertEqual(bool(color1), bool(color2))
if not color1:
return
color1 = hex_to_rgb(color1)
color2 = hex_to_rgb(color2)
self.assertEqual(len(color1), len(color2))
for i in range(len(color1)):
self.assertAlmostEqual(color1[i], color2[i], delta=self.css_color_error)
def _get_images_for_test(self):
return ['sweden.png', 'odoo.png']
def _set_images(self):
for fname in self._get_images_for_test():
fname_split = fname.split('.')
if not fname_split[0] in _file_cache:
with Image.open(os.path.join(dir_path, fname), 'r') as img:
base64_img = image_to_base64(img, 'PNG')
primary, secondary = self.env['base.document.layout'].extract_image_primary_secondary_colors(base64_img)
_img = frozendict({
'img': base64_img,
'colors': {
'primary_color': primary,
'secondary_color': secondary,
},
})
_file_cache[fname_split[0]] = _img
self.company_imgs = frozendict(_file_cache)
def _set_templates_and_layouts(self):
self.layout_template1 = self.env['ir.ui.view'].create({
'name': 'layout_template1',
'key': 'web.layout_template1',
'type': 'qweb',
'arch': '''<div></div>''',
})
self.env['ir.model.data'].create({
'name': self.layout_template1.name,
'model': 'ir.ui.view',
'module': 'web',
'res_id': self.layout_template1.id,
})
self.default_colors = {
'primary_color': '#000000',
'secondary_color': '#000000',
}
self.report_layout1 = self.env['report.layout'].create({
'view_id': self.layout_template1.id,
'name': 'report_%s' % self.layout_template1.name,
})
self.layout_template2 = self.env['ir.ui.view'].create({
'name': 'layout_template2',
'key': 'web.layout_template2',
'type': 'qweb',
'arch': '''<div></div>''',
})
self.env['ir.model.data'].create({
'name': self.layout_template2.name,
'model': 'ir.ui.view',
'module': 'web',
'res_id': self.layout_template2.id,
})
self.report_layout2 = self.env['report.layout'].create({
'view_id': self.layout_template2.id,
'name': 'report_%s' % self.layout_template2.name,
})
@tagged('document_layout', "post_install", "-at_install")
class TestBaseDocumentLayout(TestBaseDocumentLayoutHelpers):
# Logo change Tests
def test_company_no_color_change_logo(self):
"""When neither a logo nor the colors are set
The wizard displays the colors of the report layout
Changing logo means the colors on the wizard change too
Emptying the logo works and doesn't change the colors"""
self.company.write({
'primary_color': False,
'secondary_color': False,
'logo': False,
'external_report_layout_id': self.env.ref('web.layout_template1').id,
'paperformat_id': self.env.ref('base.paperformat_us').id,
})
default_colors = self.default_colors
with Form(self.env['base.document.layout']) as doc_layout:
self.assertColors(doc_layout, default_colors)
self.assertEqual(doc_layout.company_id, self.company)
doc_layout.logo = self.company_imgs['sweden']['img']
self.assertColors(doc_layout, self.company_imgs['sweden']['colors'])
doc_layout.logo = ''
self.assertColors(doc_layout, self.company_imgs['sweden']['colors'])
self.assertEqual(doc_layout.logo, '')
def test_company_no_color_but_logo_change_logo(self):
"""When company colors are not set, but a logo is,
the wizard displays the computed colors from the logo"""
self.company.write({
'primary_color': '#ff0080',
'secondary_color': '#00ff00',
'logo': self.company_imgs['sweden']['img'],
'paperformat_id': self.env.ref('base.paperformat_us').id,
})
with Form(self.env['base.document.layout']) as doc_layout:
self.assertColors(doc_layout, self.company)
doc_layout.logo = self.company_imgs['odoo']['img']
self.assertColors(doc_layout, self.company_imgs['odoo']['colors'])
def test_company_colors_change_logo(self):
"""changes of the logo implies displaying the new computed colors"""
self.company.write({
'primary_color': '#ff0080',
'secondary_color': '#00ff00',
'logo': False,
'paperformat_id': self.env.ref('base.paperformat_us').id,
})
with Form(self.env['base.document.layout']) as doc_layout:
self.assertColors(doc_layout, self.company)
doc_layout.logo = self.company_imgs['odoo']['img']
self.assertColors(doc_layout, self.company_imgs['odoo']['colors'])
def test_company_colors_and_logo_change_logo(self):
"""The colors of the company may differ from the one the logo computes
Opening the wizard in these condition displays the company's colors
When the logo changes, colors must change according to the logo"""
self.company.write({
'primary_color': '#ff0080',
'secondary_color': '#00ff00',
'logo': self.company_imgs['sweden']['img'],
'paperformat_id': self.env.ref('base.paperformat_us').id,
})
with Form(self.env['base.document.layout']) as doc_layout:
self.assertColors(doc_layout, self.company)
doc_layout.logo = self.company_imgs['odoo']['img']
self.assertColors(doc_layout, self.company_imgs['odoo']['colors'])
# Layout change tests
def test_company_colors_reset_colors(self):
"""Reset the colors when they differ from the ones originally
computed from the company logo"""
self.company.write({
'primary_color': '#ff0080',
'secondary_color': '#00ff00',
'logo': self.company_imgs['sweden']['img'],
'paperformat_id': self.env.ref('base.paperformat_us').id,
})
with Form(self.env['base.document.layout']) as doc_layout:
self.assertColors(doc_layout, self.company)
doc_layout.primary_color = doc_layout.logo_primary_color
doc_layout.secondary_color = doc_layout.logo_secondary_color
self.assertColors(doc_layout, self.company_imgs['sweden']['colors'])
def test_parse_company_colors_grayscale(self):
"""Grayscale images with transparency - make sure the color extraction does not crash"""
self.company.write({
'primary_color': '#ff0080',
'secondary_color': '#00ff00',
'paperformat_id': self.env.ref('base.paperformat_us').id,
})
with Form(self.env['base.document.layout']) as doc_layout:
with Image.open(os.path.join(dir_path, 'logo_ci.png'), 'r') as img:
base64_img = image_to_base64(img, 'PNG')
doc_layout.logo = base64_img
self.assertNotEqual(None, doc_layout.primary_color)
# /!\ This case is NOT supported, and probably not supportable
# res.partner resizes manu-militari the image it is given
# so res.company._get_logo differs from res.partner.[default image]
# def test_company_no_colors_default_logo_and_layout_change_layout(self):
# """When the default YourCompany logo is set, and no colors are set on company:
# change wizard's color according to template"""
# self.company.write({
# 'primary_color': False,
# 'secondary_color': False,
# 'external_report_layout_id': self.layout_template1.id,
# })
# default_colors = self.default_colors
# with Form(self.env['base.document.layout']) as doc_layout:
# self.assertColors(doc_layout, default_colors)
# doc_layout.report_layout_id = self.report_layout2
# self.assertColors(doc_layout, self.report_layout2)
def test_company_details_blank_lines(self):
"""Test that the company address is generated dynamically using only the fields that are defined,
without leaving any blank lines."""
# Make sure there is no blank line in the company details.
doc_layout_1 = self.env['base.document.layout'].create({'company_id': self.company.id})
self.assertFalse('\n<br>\n' in doc_layout_1.company_details)
# Make sure that 'street2' (an optional field, initially blank),
# appears in the company details when it is defined.
self.company.write({'street2': 'street_2_detail'})
doc_layout_2 = self.env['base.document.layout'].create({'company_id': self.company.id})
self.assertTrue('street_2_detail' in doc_layout_2.company_details)

View file

@ -0,0 +1,50 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import odoo.tests
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
_logger = logging.getLogger(__name__)
@odoo.tests.tagged('click_all', 'post_install', '-at_install', '-standard')
class TestMenusAdmin(odoo.tests.HttpCase):
allow_end_on_form = True
def test_01_click_everywhere_as_admin(self):
menus = self.env['ir.ui.menu'].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="admin", timeout=600)
self.terminate_browser()
@odoo.tests.tagged('click_all', 'post_install', '-at_install', '-standard')
class TestMenusDemo(odoo.tests.HttpCase):
allow_end_on_form = True
def test_01_click_everywhere_as_demo(self):
user_demo = self.env.ref("base.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()
@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',)
class TestMenusDemoLight(HttpCaseWithUserDemo):
allow_end_on_form = True
def test_01_click_apps_menus_as_demo(self):
# 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)

View file

@ -0,0 +1,119 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import operator
import re
import secrets
from unittest.mock import patch
import requests
import odoo
from odoo.tests.common import BaseCase, HttpCase, tagged
from odoo.tools import config
class TestDatabaseManager(HttpCase):
def test_database_manager(self):
if not config['list_db']:
return
res = self.url_open('/web/database/manager')
self.assertEqual(res.status_code, 200)
# check that basic existing db actions are present
self.assertIn('.o_database_backup', res.text)
self.assertIn('.o_database_duplicate', res.text)
self.assertIn('.o_database_delete', res.text)
# check that basic db actions are present
self.assertIn('.o_database_create', res.text)
self.assertIn('.o_database_restore', res.text)
@tagged('-at_install', 'post_install', '-standard', 'database_operations')
class TestDatabaseOperations(BaseCase):
def setUp(self):
self.password = secrets.token_hex()
# monkey-patch password verification
self.verify_admin_password_patcher = patch(
'odoo.tools.config.verify_admin_password', self.password.__eq__,
)
self.startPatcher(self.verify_admin_password_patcher)
self.db_name = config['db_name']
self.assertTrue(self.db_name)
# monkey-patch db-filter
self.addCleanup(operator.setitem, config, 'dbfilter', config['dbfilter'])
config['dbfilter'] = self.db_name + '.*'
self.base_databases = self.list_dbs_filtered()
self.session = requests.Session()
self.session.get(self.url('/web/database/manager'))
def tearDown(self):
self.assertEqual(
self.list_dbs_filtered(),
self.base_databases,
'No database should have been created or removed at the end of this test',
)
def list_dbs_filtered(self):
return set(db for db in odoo.service.db.list_dbs(True) if re.match(config['dbfilter'], db))
def url(self, path):
return HttpCase.base_url() + path
def assertDbs(self, dbs):
self.assertEqual(self.list_dbs_filtered() - self.base_databases, set(dbs))
def test_database_creation(self):
# check verify_admin_password patch
self.assertTrue(odoo.tools.config.verify_admin_password(self.password))
# create a database
test_db_name = self.db_name + '-test-database-creation'
self.assertNotIn(test_db_name, self.list_dbs_filtered())
res = self.session.post(self.url('/web/database/create'), data={
'master_pwd': self.password,
'name': test_db_name,
'login': 'admin',
'password': 'admin',
'lang': 'en_US',
'phone': '',
}, allow_redirects=False)
self.assertEqual(res.status_code, 303)
self.assertIn('/web', 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)
self.assertEqual(res.status_code, 303)
self.assertIn('/web/database/manager', res.headers['Location'])
self.assertDbs([])
def test_database_duplicate(self):
# duplicate this database
test_db_name = self.db_name + '-test-database-duplicate'
self.assertNotIn(test_db_name, self.list_dbs_filtered())
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)
self.assertEqual(res.status_code, 303)
self.assertIn('/web/database/manager', 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)
self.assertEqual(res.status_code, 303)
self.assertIn('/web/database/manager', res.headers['Location'])
self.assertDbs([])

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('post_install', '-at_install')
class DomainTest(HttpCaseWithUserDemo):
def test_domain_validate(self):
self.authenticate("demo", "demo")
with mute_logger('odoo.http'):
resp = self.url_open(
'/web/domain/validate',
headers={'Content-Type': 'application/json'},
data=json.dumps({'params': {'model':'i', 'domain':[]}}),
)
self.assertEqual(resp.json()['error']['data']['message'], "Invalid model: i")
resp = self.url_open(
'/web/domain/validate',
headers={'Content-Type': 'application/json'},
data=json.dumps({'params': {'model':'res.users', 'domain':[]}}),
)
self.assertEqual(resp.json()['result'], True)
resp = self.url_open(
'/web/domain/validate',
headers={'Content-Type': 'application/json'},
data=json.dumps({'params': {'model':'res.users', 'domain':[('name', 'ilike', 'ad')]}}),
)
self.assertEqual(resp.json()['result'], True)
resp = self.url_open(
'/web/domain/validate',
headers={'Content-Type': 'application/json'},
data=json.dumps({'params': {'model':'res.users', 'domain':[('hop')]}}),
)
self.assertEqual(resp.json()['result'], False)

View file

@ -0,0 +1,32 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import psycopg2
from unittest.mock import patch
from odoo.tests import HttpCase
class TestWebController(HttpCase):
def test_health(self):
response = self.url_open('/web/health')
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['status'], 'pass')
self.assertFalse(response.cookies.get('session_id'))
def test_health_db_server_status(self):
response = self.url_open('/web/health?db_server_status=1')
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['status'], 'pass')
self.assertEqual(payload['db_server_status'], True)
self.assertFalse(response.cookies.get('session_id'))
def _raise_psycopg2_error(*args):
raise psycopg2.Error('boom')
with patch('odoo.sql_db.db_connect', new=_raise_psycopg2_error):
response = self.url_open('/web/health?db_server_status=1')
self.assertEqual(response.status_code, 500)
payload = response.json()
self.assertEqual(payload['status'], 'fail')
self.assertEqual(payload['db_server_status'], False)

View file

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import base64
from PIL import Image
from werkzeug.urls import url_unquote_plus
from odoo.tests.common import HttpCase, tagged
@tagged('-at_install', 'post_install')
class TestImage(HttpCase):
def test_01_content_image_resize_placeholder(self):
"""The goal of this test is to make sure the placeholder image is
resized appropriately depending on the given URL parameters."""
# CASE: resize placeholder, given size but original ratio is always kept
response = self.url_open('/web/image/0/200x150')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (150, 150))
# CASE: resize placeholder to 128
response = self.url_open('/web/image/fake/0/image_128')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (128, 128))
# CASE: resize placeholder to 256
response = self.url_open('/web/image/fake/0/image_256')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (256, 256))
# CASE: resize placeholder to 1024 (but placeholder image is too small)
response = self.url_open('/web/image/fake/0/image_1024')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (256, 256))
# CASE: no size found, use placeholder original size
response = self.url_open('/web/image/fake/0/image_no_size')
response.raise_for_status()
image = Image.open(io.BytesIO(response.content))
self.assertEqual(image.size, (256, 256))
def test_02_content_image_Etag_304(self):
"""This test makes sure that the 304 response is properly returned if the ETag is properly set"""
attachment = self.env['ir.attachment'].create({
'datas': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
'name': 'testEtag.gif',
'public': True,
'mimetype': 'image/gif',
})
response = self.url_open('/web/image/%s' % attachment.id, timeout=None)
response.raise_for_status()
self.assertEqual(response.status_code, 200)
self.assertEqual(base64.b64encode(response.content), attachment.datas)
etag = response.headers.get('ETag')
response2 = self.url_open('/web/image/%s' % attachment.id, headers={"If-None-Match": etag})
response2.raise_for_status()
self.assertEqual(response2.status_code, 304)
self.assertEqual(len(response2.content), 0)
def test_03_web_content_filename(self):
"""This test makes sure the Content-Disposition header matches the given filename"""
att = self.env['ir.attachment'].create({
'datas': b'R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=',
'name': 'testFilename.gif',
'public': True,
'mimetype': 'image/gif'
})
# CASE: no filename given
res = self.url_open('/web/image/%s/0x0/?download=true' % att.id)
res.raise_for_status()
self.assertEqual(res.headers['Content-Disposition'], 'attachment; filename=testFilename.gif')
# CASE: given filename without extension
res = self.url_open('/web/image/%s/0x0/custom?download=true' % att.id)
res.raise_for_status()
self.assertEqual(res.headers['Content-Disposition'], 'attachment; filename=custom.gif')
# CASE: given filename and extention
res = self.url_open('/web/image/%s/0x0/custom.png?download=true' % att.id)
res.raise_for_status()
self.assertEqual(res.headers['Content-Disposition'], 'attachment; filename=custom.png')
def test_04_web_content_filename_secure(self):
"""This test makes sure the Content-Disposition header matches the given filename"""
att = self.env['ir.attachment'].create({
'datas': b'R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=',
'name': """fô☺o-l'éb \n a"!r".gif""",
'public': True,
'mimetype': 'image/gif',
})
def remove_prefix(text, prefix):
if text.startswith(prefix):
return text[len(prefix):]
return text
def assert_filenames(
url,
expected_filename,
expected_filename_star='',
message=r"File that will be saved on disc should have the original filename without \n and \r",
):
res = self.url_open(url)
res.raise_for_status()
if expected_filename_star:
inline, filename, filename_star = res.headers['Content-Disposition'].split('; ')
else:
inline, filename = res.headers['Content-Disposition'].split('; ')
filename_star = ''
filename = remove_prefix(filename, "filename=").strip('"')
filename_star = url_unquote_plus(remove_prefix(filename_star, "filename*=UTF-8''").strip('"'))
self.assertEqual(inline, 'inline')
self.assertEqual(filename, expected_filename, message)
self.assertEqual(filename_star, expected_filename_star, message)
assert_filenames(f'/web/image/{att.id}',
r"""foo-l'eb _ a\"!r\".gif""",
r"""fô☺o-l'éb _ a"!r".gif""",
)
assert_filenames(f'/web/image/{att.id}/custom_invalid_name\nis-ok.gif',
r"""custom_invalid_name_is-ok.gif""",
)
assert_filenames(f'/web/image/{att.id}/\r\n',
r"""__.gif""",
)
assert_filenames(f'/web/image/{att.id}/你好',
r""".gif""",
r"""你好.gif""",
)
assert_filenames(f'/web/image/{att.id}/%E9%9D%A2%E5%9B%BE.gif',
r""".gif""",
r"""面图.gif""",
)
assert_filenames(f'/web/image/{att.id}/hindi_नमस्ते.gif',
r"""hindi_.gif""",
r"""hindi_नमस्ते.gif""",
)
assert_filenames(f'/web/image/{att.id}/arabic_مرحبا',
r"""arabic_.gif""",
r"""arabic_مرحبا.gif""",
)
assert_filenames(f'/web/image/{att.id}/4wzb_!!63148-0-t1.jpg_360x1Q75.jpg_.webp',
r"""4wzb_!!63148-0-t1.jpg_360x1Q75.jpg_.webp""",
)

View file

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
from odoo.tests import tagged
from odoo.tests.common import new_test_user
@tagged("post_install", "-at_install")
class IrModelAccessTest(TransactionCase):
@classmethod
def setUpClass(cls):
super(IrModelAccessTest, cls).setUpClass()
cls.env['ir.model.access'].create({
'name': "read",
'model_id': cls.env['ir.model'].search([("model", "=", "res.company")]).id,
'group_id': cls.env.ref("base.group_public").id,
'perm_read': False,
})
cls.env['ir.model.access'].create({
'name': "read",
'model_id': cls.env['ir.model'].search([("model", "=", "res.company")]).id,
'group_id': cls.env.ref("base.group_portal").id,
'perm_read': True,
})
cls.env['ir.model.access'].create({
'name': "read",
'model_id': cls.env['ir.model'].search([("model", "=", "res.company")]).id,
'group_id': cls.env.ref("base.group_user").id,
'perm_read': True,
})
cls.portal_user = new_test_user(
cls.env, login="portalDude", groups="base.group_portal"
)
cls.public_user = new_test_user(
cls.env, login="publicDude", groups="base.group_public"
)
cls.spreadsheet_user = new_test_user(
cls.env, login="spreadsheetDude", groups="base.group_user"
)
def test_display_name_for(self):
# Internal User with access rights can access the business name
result = self.env['ir.model'].with_user(self.spreadsheet_user).display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}])
# external user with access rights cannot access business name
result = self.env['ir.model'].with_user(self.portal_user).display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "res.company", "model": "res.company"}])
# external user without access rights cannot access business name
result = self.env['ir.model'].with_user(self.public_user).display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "res.company", "model": "res.company"}])
# admin has all rights
result = self.env['ir.model'].display_name_for(["res.company"])
self.assertEqual(result, [{"display_name": "Companies", "model": "res.company"}])
# non existent model yields same result as a lack of access rights
result = self.env['ir.model'].display_name_for(["unexistent"])
self.assertEqual(result, [{"display_name": "unexistent", "model": "unexistent"}])
# 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"}])

View file

@ -0,0 +1,30 @@
from lxml import etree
from odoo.tests.common import TransactionCase
class TestIrQweb(TransactionCase):
def test_image_field(self):
view = self.env["ir.ui.view"].create({
"key": "web.test_qweb",
"type": "qweb",
"arch": """<t t-name="test_qweb">
<span t-field="record.avatar_128" t-options-widget="'image'" t-options-qweb_img_raw_data="is_raw_image" />
</t>"""
})
partner = self.env["res.partner"].create({
"name": "test image partner",
"image_128": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVR4nGJxKFrEwMDAxAAGgAAAAP//D+IBWx9K7TUAAAAASUVORK5CYII=",
})
html = view._render_template(view.id, {"is_raw_image": True, "record": partner})
tree = etree.fromstring(html)
img = tree.find("img")
self.assertTrue(img.get("src").startswith("data:image/png;base64"))
self.assertEqual(img.get("class"), "img img-fluid")
self.assertEqual(img.get("alt"), "test image partner")
html = view._render_template(view.id, {"is_raw_image": False, "record": partner})
tree = etree.fromstring(html)
img = tree.find("img")
self.assertTrue(img.get("src").startswith("/web/image"))
self.assertEqual(img.get("class"), "img img-fluid")
self.assertEqual(img.get("alt"), "test image partner")

View file

@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import odoo.tests
from werkzeug.urls import url_quote_plus
RE_ONLY = re.compile(r'QUnit\.(only|debug)\(')
def qunit_error_checker(message):
# We don't want to stop qunit if a qunit is breaking.
# '%s/%s test failed.' case: end message when all tests are finished
if 'tests failed.' in message:
return True
# "QUnit test failed" case: one qunit failed. don't stop in this case
if "QUnit test failed:" in message:
return False
return True # in other cases, always stop (missing dependency, ...)
@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)}.*)')
filter = ''
if positive or negative:
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)
self.assertNotRegex('utils > bl1', f)
self.assertNotRegex('utils > bl2', f)
self.assertNotRegex('web', f)
f2 = self.get_filter([('+', '-utils > bl1,-utils > bl2')])
f3 = self.get_filter([('-', 'utils > bl1,utils > bl2')])
for f in (f2, f3):
self.assertRegex('utils', f)
self.assertRegex('mail', f)
self.assertRegex('utils > something', f)
self.assertRegex('web', f)
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 WebSuite(WebsuiteCommon):
@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_check_suite(self):
# verify no js test is using `QUnit.only` as it forbid any other test to be executed
self._check_only_call('web.qunit_suite_tests')
self._check_only_call('web.qunit_mobile_suite_tests')
def _check_only_call(self, suite):
# As we currently aren't in a request context, we can't render `web.layout`.
# redefinied it as a minimal proxy template.
self.env.ref('web.layout').write({'arch_db': '<t t-name="web.layout"><head><meta charset="utf-8"/><t t-esc="head"/></head></t>'})
assets = self.env['ir.qweb']._get_asset_content(suite)[0]
if len(assets) == 0:
self.fail("No assets found in the given test suite")
for asset in assets:
filename = asset['filename']
if not filename or asset['atype'] != 'text/javascript':
continue
with open(filename, 'rb') as fp:
if RE_ONLY.search(fp.read().decode('utf-8')):
self.fail("`QUnit.only()` or `QUnit.debug()` used in file %r" % asset['url'])
@odoo.tests.tagged('post_install', '-at_install')
class MobileWebSuite(WebsuiteCommon):
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)

View file

@ -0,0 +1,53 @@
from odoo.tests.common import HttpCase
class LoadMenusTests(HttpCase):
def setUp(self):
super().setUp()
self.menu = self.env["ir.ui.menu"].create({
"name": "test_menu",
"parent_id": False,
})
def search(*args, **kwargs):
return self.menu
self.patch(type(self.env["ir.ui.menu"]), "search", search)
self.authenticate("admin", "admin")
def test_load_menus(self):
menu_loaded = self.url_open("/web/webclient/load_menus/1234")
expected = {
str(self.menu.id): {
"actionID": False,
"actionModel": False,
"appID": self.menu.id,
"children": [],
"id": self.menu.id,
"name": "test_menu",
"webIcon": False,
"webIconData": False,
"xmlid": ""
},
"root": {
"actionID": False,
"actionModel": False,
"appID": False,
"children": [
self.menu.id,
],
"id": "root",
"name": "root",
"webIcon": None,
"webIconData": None,
"xmlid": "",
"backgroundImage": None,
}
}
self.assertDictEqual(
menu_loaded.json(),
expected,
"load_menus didn't return the expected value"
)

View file

@ -0,0 +1,60 @@
# 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
class TestWebLoginCommon(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
new_test_user(cls.env, 'internal_user', context={'lang': 'en_US'})
new_test_user(cls.env, 'portal_user', groups='base.group_portal')
def setUp(self):
super().setUp()
self.session = http.root.session_store.new()
self.session.update(http.get_default_session(), db=get_db_name())
self.opener = Opener(self.env.cr)
self.opener.cookies.set('session_id', self.session.sid, domain=HOST, path='/')
def login(self, username, password, csrf_token=None):
"""Log in with provided credentials and return response to POST request or raises for status."""
res_post = self.url_open('/web/login', data={
'login': username,
'password': password,
'csrf_token':csrf_token or http.Request.csrf_token(self),
})
res_post.raise_for_status()
return res_post
class TestWebLogin(TestWebLoginCommon):
def test_web_login(self):
res_post = self.login('internal_user', 'internal_user')
# ensure we are logged-in
self.url_open(
'/web/session/check',
headers={'Content-Type': 'application/json'},
data='{}'
).raise_for_status()
# ensure we end up on the right page for internal users.
self.assertEqual(res_post.request.path_url, '/web')
def test_web_login_external(self):
res_post = self.login('portal_user', 'portal_user')
# ensure we end up on the right page for external users. Valid without portal installed.
self.assertEqual(res_post.request.path_url, '/web/login_successful')
def test_web_login_bad_xhr(self):
# simulate the user downloaded the login form
csrf_token = http.Request.csrf_token(self)
# simulate that the JS sended a bad XHR to a route that is
# auth='none' using the same session (e.g. via a service worker)
bad_xhr = self.url_open('/web/login_successful', allow_redirects=False)
self.assertNotEqual(bad_xhr.status_code, 200)
# log in using the above form, it should still be valid
self.login('internal_user', 'internal_user', csrf_token)

View file

@ -0,0 +1,54 @@
# 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"
})

View file

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import json
from unittest.mock import patch
from odoo.tools import mute_logger
from odoo.tests.common import HttpCase, tagged
class ProfilingHttpCase(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Trick: we patch db_connect() to make it return the registry; when the
# profiler calls cursor() on it, it gets a test cursor (with cls.cr as
# its actual cursor), which prevents the profiling data from being
# committed for real.
cls.patcher = patch('odoo.sql_db.db_connect', return_value=cls.registry)
cls.startClassPatcher(cls.patcher)
def profile_rpc(self, params=None):
params = params or {}
req = self.url_open(
'/web/dataset/call_kw/ir.profile/set_profiling', # use model and method in route has web client does
headers={'Content-Type': 'application/json'},
data=json.dumps({'params':{
'model': 'ir.profile',
'method': 'set_profiling',
'args': [],
'kwargs': params,
}})
)
req.raise_for_status()
return req.json()
@tagged('post_install', '-at_install', 'profiling')
class TestProfilingWeb(ProfilingHttpCase):
def test_profiling_enabled(self):
# since profiling will use a direct connection to the database patch 'db_connect' to ensure we are using the test cursor
self.authenticate('admin', 'admin')
last_profile = self.env['ir.profile'].search([], limit=1, order='id desc')
# Trying to start profiling when not enabled
self.env['ir.config_parameter'].set_param('base.profiling_enabled_until', '')
res = self.profile_rpc({'profile': 1})
self.assertEqual(res['result']['res_model'], 'base.enable.profiling.wizard')
self.assertEqual(last_profile, self.env['ir.profile'].search([], limit=1, order='id desc'))
# Enable profiling and start blank profiling
expiration = datetime.datetime.now() + datetime.timedelta(seconds=50)
self.env['ir.config_parameter'].set_param('base.profiling_enabled_until', expiration)
res = self.profile_rpc({'profile': 1})
self.assertTrue(res['result']['session'])
self.assertEqual(last_profile, self.env['ir.profile'].search([], limit=1, order='id desc'), "profiling route shouldn't have been profiled")
# Profile a page
res = self.url_open('/web/speedscope') # profile a light route
new_profile = self.env['ir.profile'].search([], limit=1, order='id desc')
self.assertNotEqual(last_profile, new_profile, "A new profile should have been created")
self.assertEqual(new_profile.name, '/web/speedscope?')
def test_profile_test_tool(self):
with self.profile():
self.url_open('/web')
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?',
])
@tagged('post_install', '-at_install', 'profiling')
class TestProfilingModes(ProfilingHttpCase):
def test_profile_collectors(self):
expiration = datetime.datetime.now() + datetime.timedelta(seconds=50)
self.env['ir.config_parameter'].set_param('base.profiling_enabled_until', expiration)
self.authenticate('admin', 'admin')
res = self.profile_rpc({})
self.assertEqual(res['result']['collectors'], None)
res = self.profile_rpc({'profile': 1, 'collectors': ['sql', 'traces_async']})
self.assertEqual(sorted(res['result']['collectors']), ['sql', 'traces_async'])
res = self.profile_rpc({'collectors': ['sql']})
self.assertEqual(res['result']['collectors'], ['sql'],)
res = self.profile_rpc({'profile': 0})
res = self.profile_rpc({'profile': 1})
self.assertEqual(res['result']['collectors'], ['sql'],
"Enabling and disabling profiling shouldn't have change existing preferences")
@tagged('post_install', '-at_install', 'profiling')
class TestProfilingPublic(ProfilingHttpCase):
def test_public_user_profiling(self):
last_profile = self.env['ir.profile'].search([], limit=1, order='id desc')
self.env['ir.config_parameter'].set_param('base.profiling_enabled_until', '')
self.authenticate(None, None)
res = self.url_open('/web/set_profiling?profile=1')
self.assertEqual(res.status_code, 500)
self.assertEqual(res.text, 'error: Profiling is not enabled on this database. Please contact an administrator.')
expiration = datetime.datetime.now() + datetime.timedelta(seconds=50)
self.env['ir.config_parameter'].set_param('base.profiling_enabled_until', expiration)
res = self.url_open('/web/set_profiling?profile=1')
self.assertEqual(res.status_code, 200)
res = res.json()
self.assertTrue(res.pop('session'))
self.assertEqual(res, {"collectors": ["sql", "traces_async"], "params": {}})
self.assertEqual(last_profile, self.env['ir.profile'].search([], limit=1, order='id desc'), "profiling route shouldn't have been profiled")
res = self.url_open('/web/login') # profile /web/login to avoid redirections of /
new_profile = self.env['ir.profile'].search([], limit=1, order='id desc')
self.assertNotEqual(last_profile, new_profile, "A route should have been profiled")
self.assertEqual(new_profile.name, '/web/login?')

View file

@ -0,0 +1,158 @@
# -*- 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},
})

View file

@ -0,0 +1,76 @@
import odoo.tests
from odoo.addons.website.tools import MockRequest
class TestReports(odoo.tests.HttpCase):
def test_report_session_cookie(self):
""" Asserts wkhtmltopdf forwards the user session when requesting resources to Odoo, such as images,
and that the resource is correctly returned as expected.
"""
partner_id = self.env.user.partner_id.id
img = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC'
image = self.env['ir.attachment'].create({
'name': 'foo',
'res_model': 'res.partner',
'res_id': partner_id,
'datas': img,
})
report = self.env['ir.actions.report'].create({
'name': 'test report',
'report_name': 'base.test_report',
'model': 'res.partner',
})
self.env['ir.ui.view'].create({
'type': 'qweb',
'name': 'base.test_report',
'key': 'base.test_report',
'arch': f'''
<main>
<div class="article" data-oe-model="res.partner" t-att-data-oe-id="docs.id">
<img src="/web/image/{image.id}"/>
</div>
</main>
'''
})
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):
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)
result.update({'record_id': record.id, 'data': record.datas})
else:
record = origin_find_record(self, xmlid, res_model, res_id, access_token)
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')
report = report.with_user(admin)
with MockRequest(report.env) as mock_request:
mock_request.session.sid = self.authenticate(admin.login, admin.login).sid
report.with_context(force_report_rendering=True)._render_qweb_pdf(report.id, [partner_id])
self.assertEqual(
result.get('uid'), admin.id, 'wkhtmltopdf is not fetching the image as the user printing the report'
)
self.assertEqual(result.get('record_id'), image.id, 'wkhtmltopdf did not fetch the expected record')
self.assertEqual(result.get('data'), img, 'wkhtmltopdf did not fetch the right image content')
# 2. Request the report as public, who has no acess to the image
self.logout()
result.clear()
public = self.env.ref('base.public_user')
report = report.with_user(public)
with MockRequest(self.env) as mock_request:
report.with_context(force_report_rendering=True)._render_qweb_pdf(report.id, [partner_id])
self.assertEqual(
result.get('uid'), public.id, 'wkhtmltopdf is not fetching the image as the user printing the report'
)
self.assertEqual(result.get('record_id'), None, 'wkhtmltopdf must not have been allowed to fetch the image')
self.assertEqual(result.get('data'), None, 'wkhtmltopdf must not have been allowed to fetch the image')

View file

@ -0,0 +1,74 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from uuid import uuid4
from odoo import Command
from odoo.tests import common
class TestSessionInfo(common.HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
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.user_password = "info"
cls.user = common.new_test_user(
cls.env,
"session",
email="session@in.fo",
password=cls.user_password,
tz="UTC")
cls.user.write({
'company_id': cls.company_a.id,
'company_ids': [Command.set([company.id for company in cls.companies])],
})
cls.payload = json.dumps(dict(jsonrpc="2.0", method="call", id=str(uuid4())))
cls.headers = {
"Content-Type": "application/json",
}
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)
response = self.url_open("/web/session/get_session_info", data=self.payload, headers=self.headers)
self.assertEqual(response.status_code, 200)
data = response.json()
result = data["result"]
expected_allowed_companies = {
str(company.id): {
'id': company.id,
'name': company.name,
'sequence': company.sequence,
} for company in self.companies
}
expected_user_companies = {
'current_company': self.company_a.id,
'allowed_companies': expected_allowed_companies,
}
self.assertEqual(
result['user_companies'],
expected_user_companies,
"The session_info['user_companies'] does not have the expected structure")
def test_session_modules(self):
self.authenticate(self.user.login, self.user_password)
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()

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from odoo.tests import common
from unittest.mock import patch
@common.tagged('post_install', '-at_install')
class TestWebSearchRead(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
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):
original_search_count = self.ResCurrency.search_count
search_count_called = [False]
def search_count(obj, *method_args, **method_kwargs):
search_count_called[0] = True
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)
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):
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)
self.assert_web_search_read(2, 2, limit=2, count_limit=2, expected_search_count_called=False)
self.assert_web_search_read(20, 2, limit=2, offset=10, count_limit=20)
self.assert_web_search_read(12, 2, limit=2, offset=10, count_limit=12, expected_search_count_called=False)