mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 04:12:02 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
|
|
@ -5,17 +5,25 @@ 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_favorite
|
||||
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_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_perf_load_menu
|
||||
from . import test_pivot_export
|
||||
from . import test_res_partner_properties
|
||||
from . import test_action
|
||||
|
|
|
|||
275
odoo-bringout-oca-ocb-web/web/tests/test_action.py
Normal file
275
odoo-bringout-oca-ocb-web/web/tests/test_action.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import json
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestLoadBreadcrumbs(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
model_id = cls.env['ir.model']._get_id('res.partner')
|
||||
|
||||
# Create a user in order to have a fixed name in local and on the runbot
|
||||
cls.env = cls.env(user=cls.env.ref('base.user_admin'))
|
||||
cls.partner = cls.env['res.partner'].create({
|
||||
'name': 'Test Partner',
|
||||
})
|
||||
|
||||
cls.window_action = cls.env['ir.actions.act_window'].create({
|
||||
'name': 'Test Partners',
|
||||
'res_model': 'res.partner',
|
||||
})
|
||||
|
||||
cls.server_action_without_path = cls.env['ir.actions.server'].create({
|
||||
'name': 'Test Server Action Without Path',
|
||||
'model_id': model_id,
|
||||
'state': 'code',
|
||||
'code': """action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Window Action From Server',
|
||||
'res_model': 'res.partner',
|
||||
}""",
|
||||
|
||||
})
|
||||
|
||||
cls.server_action = cls.env['ir.actions.server'].create({
|
||||
'name': 'Breadcrumb Server Action',
|
||||
'model_id': model_id,
|
||||
'state': 'code',
|
||||
'path': 'test_path',
|
||||
'code': """action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Window Action From Server',
|
||||
'res_model': 'res.partner',
|
||||
'views': [(False, 'list')],
|
||||
}""",
|
||||
})
|
||||
|
||||
cls.client_action = cls.env['ir.actions.client'].create({
|
||||
'name': 'Breadcrumb Client Action',
|
||||
'res_model': 'res.partner',
|
||||
'tag': 'account_report',
|
||||
})
|
||||
|
||||
cls.server_action_with_form_view = cls.env['ir.actions.server'].create({
|
||||
'name': 'Breadcrumb Server Action With Path',
|
||||
'model_id': model_id,
|
||||
'state': 'code',
|
||||
'path': 'test_path_form_view',
|
||||
'code': """action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Window Action From Server',
|
||||
'res_model': 'res.partner',
|
||||
'views': [(False, 'form')],
|
||||
}""",
|
||||
})
|
||||
|
||||
def test_breadcrumbs_empty_action(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'], [])
|
||||
|
||||
def test_breadcrumbs_window_action(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'action': self.window_action.id,
|
||||
'resId': None,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['display_name'], 'Test Partners')
|
||||
|
||||
def test_breadcrumbs_server_action_path(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'action': self.server_action_without_path.id,
|
||||
'resId': None,
|
||||
},
|
||||
{
|
||||
'action': self.server_action.id,
|
||||
'resId': None,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['error'], 'A server action must have a path to be restored')
|
||||
self.assertEqual(resp.json()['result'][1]['display_name'], 'Window Action From Server')
|
||||
|
||||
def test_breadcrumbs_client_action(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'action': self.client_action.id,
|
||||
'resId': None,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['display_name'], 'Breadcrumb Client Action')
|
||||
|
||||
def test_breadcrumbs_client_action_multirecord(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'action': self.client_action.id,
|
||||
'resId': None,
|
||||
},
|
||||
{
|
||||
'action': self.client_action.id,
|
||||
'resId': 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['error'], 'Client actions don\'t have multi-record views')
|
||||
|
||||
def test_breadcrumbs_action_with_res_model(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'action': self.client_action.id,
|
||||
'resId': 'new',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['display_name'], 'New')
|
||||
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'action': self.client_action.id,
|
||||
'resId': self.partner.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['display_name'], 'Test Partner')
|
||||
|
||||
def test_breadcrumbs_server_action_without_res_model(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'action': self.server_action.id,
|
||||
'resId': None,
|
||||
},
|
||||
{
|
||||
'action': self.server_action_with_form_view.id,
|
||||
'resId': None,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['display_name'], 'Window Action From Server')
|
||||
self.assertEqual(resp.json()['result'][1]['display_name'], None)
|
||||
|
||||
def test_breadcrumbs_get_model(self):
|
||||
self.authenticate("admin", "admin")
|
||||
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'resId': None,
|
||||
'model': 'res.users',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['error']['message'], 'Odoo Server Error')
|
||||
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'resId': self.partner.id,
|
||||
'model': 'res.partner',
|
||||
},
|
||||
{
|
||||
'resId': 'new',
|
||||
'model': 'res.partner',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['result'][0]['display_name'], self.partner.display_name)
|
||||
self.assertEqual(resp.json()['result'][1]['display_name'], 'New')
|
||||
|
||||
def test_breadcrumbs_no_action_nor_model(self):
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self.url_open(
|
||||
'/web/action/load_breadcrumbs',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps({
|
||||
'params': {
|
||||
'actions': [
|
||||
{
|
||||
'resId': None,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
self.assertEqual(resp.json()['error']['message'], 'Odoo Server Error')
|
||||
|
|
@ -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,82 @@ 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)
|
||||
|
||||
return cursors
|
||||
|
||||
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.set_registry_readonly_mode(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,49 +2,87 @@
|
|||
|
||||
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__)
|
||||
|
||||
|
||||
@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):
|
||||
if 'tour_enabled' in self.env['res.users']._fields:
|
||||
self.env.ref('base.user_admin').tour_enabled = False
|
||||
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()
|
||||
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):
|
||||
allow_end_on_form = True
|
||||
class TestMenusDemo(HttpCaseWithUserDemo):
|
||||
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/v2/get_dashboard_institutions' in r.url:
|
||||
r = Response()
|
||||
r.status_code = 200
|
||||
r.json = list
|
||||
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.prep.display' in self.env:
|
||||
self.env['pos.prep.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
|
||||
|
||||
|
|
@ -41,8 +44,8 @@ class TestDatabaseOperations(BaseCase):
|
|||
)
|
||||
self.startPatcher(self.verify_admin_password_patcher)
|
||||
|
||||
self.db_name = config['db_name']
|
||||
self.assertTrue(self.db_name)
|
||||
self.assertEqual(len(config['db_name']), 1)
|
||||
self.db_name = config['db_name'][0]
|
||||
|
||||
# monkey-patch db-filter
|
||||
self.addCleanup(operator.setitem, config, 'dbfilter', config['dbfilter'])
|
||||
|
|
@ -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, {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\.$"
|
||||
))
|
||||
|
|
|
|||
8
odoo-bringout-oca-ocb-web/web/tests/test_favorite.py
Normal file
8
odoo-bringout-oca-ocb-web/web/tests/test_favorite.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from odoo.tests.common import HttpCase, tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestFavorite(HttpCase):
|
||||
def test_favorite_management(self):
|
||||
self.patch(self.env.registry.get("ir.module.module"), "_order", "sequence desc, id desc")
|
||||
self.env["ir.module.module"]._get("l10n_fr").sequence = 100000
|
||||
self.start_tour("/odoo/apps", "test_favorite_management", login="admin")
|
||||
|
|
@ -4,10 +4,13 @@
|
|||
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.tests.common import HttpCase, tagged
|
||||
from odoo.tests.common import HttpCase, new_test_user, tagged
|
||||
from odoo.tools.misc import limited_field_access_token
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
|
|
@ -157,3 +160,191 @@ 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))
|
||||
|
||||
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")
|
||||
# token from a different scope: ko
|
||||
token = limited_field_access_token(attachment, "raw", scope="other_scope")
|
||||
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")
|
||||
# valid token: ok
|
||||
token = attachment._get_raw_access_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=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_token(self.env["ir.attachment"].browse(2)._get_raw_access_token()),
|
||||
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_token(
|
||||
self.env["ir.attachment"].browse(2)._get_raw_access_token()
|
||||
),
|
||||
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_token(
|
||||
self.env["ir.attachment"].browse(2)._get_raw_access_token()
|
||||
),
|
||||
base_result,
|
||||
)
|
||||
# a different record generates a different token
|
||||
record_res = self.env["ir.attachment"].browse(3)._get_raw_access_token()
|
||||
self.assertNotIn(record_res, [base_result])
|
||||
# a different field generates a different token
|
||||
field_res = get_datetime_from_token(
|
||||
limited_field_access_token(self.env["ir.attachment"].browse(3), "datas", scope="binary")
|
||||
)
|
||||
self.assertNotIn(field_res, [base_result, record_res])
|
||||
# a different model generates a different token
|
||||
model_res = get_datetime_from_token(
|
||||
limited_field_access_token(self.env["res.partner"].browse(3), "raw", scope="binary")
|
||||
)
|
||||
self.assertNotIn(model_res, [base_result, record_res, field_res])
|
||||
|
||||
def test_06_web_image_attachment_access(self):
|
||||
"""Tests all the combination of user/ways to access an attachment through `/web/content`
|
||||
or `/web/image` routes"""
|
||||
new_test_user(self.env, "portal_user", groups="base.group_portal")
|
||||
new_test_user(self.env, "internal_user")
|
||||
# record of arbitrary model with restrictive ACL even for internal users
|
||||
restricted_record = self.env["res.users.settings"].create({"user_id": self.env.user.id})
|
||||
# record of arbitrary model with permissive ACL for internal users
|
||||
accessible_record = self.env["res.partner"].create({"name": "test partner"})
|
||||
attachments = self.env["ir.attachment"].create(
|
||||
[
|
||||
{
|
||||
"datas": b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
||||
"description": "restricted attachment",
|
||||
"name": "test.gif",
|
||||
"res_id": restricted_record.id,
|
||||
"res_model": restricted_record._name,
|
||||
},
|
||||
{
|
||||
"datas": b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
||||
"description": "restricted attachment",
|
||||
"name": "test.gif",
|
||||
"res_id": accessible_record.id,
|
||||
"res_model": accessible_record._name,
|
||||
},
|
||||
{
|
||||
"datas": b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
||||
"description": "standalone attachment",
|
||||
"name": "test.gif",
|
||||
},
|
||||
{
|
||||
"datas": b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
||||
"description": "public attachment",
|
||||
"name": "test.gif",
|
||||
"public": True,
|
||||
},
|
||||
]
|
||||
)
|
||||
attachments.generate_access_token()
|
||||
internal_restricted, internal_accessible, standalone, public = attachments
|
||||
tests = [
|
||||
# (attachment, user, token, expected result (True if accessible))
|
||||
(internal_restricted, "public_user", None, False),
|
||||
(internal_restricted, "public_user", "token", True),
|
||||
(internal_restricted, "public_user", "limited token", True),
|
||||
(internal_restricted, "portal_user", None, False),
|
||||
(internal_restricted, "portal_user", "token", True),
|
||||
(internal_restricted, "portal_user", "limited token", True),
|
||||
(internal_restricted, "internal_user", None, False),
|
||||
(internal_restricted, "internal_user", "token", True),
|
||||
(internal_restricted, "internal_user", "limited token", True),
|
||||
(internal_accessible, "public_user", None, False),
|
||||
(internal_accessible, "public_user", "token", True),
|
||||
(internal_accessible, "public_user", "limited token", True),
|
||||
(internal_accessible, "portal_user", None, False),
|
||||
(internal_accessible, "portal_user", "token", True),
|
||||
(internal_accessible, "portal_user", "limited token", True),
|
||||
(internal_accessible, "internal_user", None, True),
|
||||
(internal_accessible, "internal_user", "token", True),
|
||||
(internal_accessible, "internal_user", "limited token", True),
|
||||
(standalone, "public_user", None, False),
|
||||
(standalone, "public_user", "token", True),
|
||||
(standalone, "public_user", "limited token", True),
|
||||
(standalone, "portal_user", None, False),
|
||||
(standalone, "portal_user", "token", True),
|
||||
(standalone, "portal_user", "limited token", True),
|
||||
(standalone, "internal_user", None, False),
|
||||
(standalone, "internal_user", "token", True),
|
||||
(standalone, "internal_user", "limited token", True),
|
||||
(public, "public_user", None, True),
|
||||
(public, "public_user", "token", True),
|
||||
(public, "public_user", "limited token", True),
|
||||
(public, "portal_user", None, True),
|
||||
(public, "portal_user", "token", True),
|
||||
(public, "portal_user", "limited token", True),
|
||||
(public, "internal_user", None, True),
|
||||
(public, "internal_user", "token", True),
|
||||
(public, "internal_user", "limited token", True),
|
||||
]
|
||||
for attachment, user, token, result in tests:
|
||||
login = None if user == "public_user" else user
|
||||
self.authenticate(login, login)
|
||||
access_token_param = ""
|
||||
if token:
|
||||
access_token = (
|
||||
attachment.access_token
|
||||
if token == "token"
|
||||
else attachment._get_raw_access_token()
|
||||
)
|
||||
access_token_param = f"?access_token={access_token}"
|
||||
res = self.url_open(f"/web/image/{attachment.id}{access_token_param}")
|
||||
if result:
|
||||
self.assertEqual(
|
||||
res.headers["Content-Disposition"],
|
||||
"inline; filename=test.gif",
|
||||
f"{user} should have access to {attachment.description} with {token or 'no token'}",
|
||||
)
|
||||
else:
|
||||
self.assertEqual(
|
||||
res.headers["Content-Disposition"],
|
||||
"inline; filename=placeholder.png",
|
||||
f"{user} should not have access to {attachment.description} with {token or 'no token'}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import Command
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
|
@ -63,3 +65,225 @@ 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)
|
||||
|
||||
|
||||
class TestIrModel(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# The test mode is necessary in this case. After each test, we call
|
||||
# registry.reset_changes(), which opens a new cursor to retrieve custom
|
||||
# models and fields. A regular cursor would correspond to the state of
|
||||
# the database before setUpClass(), which is not correct. Instead, a
|
||||
# test cursor will correspond to the state of the database of cls.cr at
|
||||
# that point, i.e., before the call to setUp().
|
||||
cls.registry_enter_test_mode_cls()
|
||||
|
||||
# model and records for banana stages
|
||||
cls.env['ir.model'].create({
|
||||
'name': 'Banana Ripeness',
|
||||
'model': 'x_banana_ripeness',
|
||||
'field_id': [
|
||||
Command.create({'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}),
|
||||
]
|
||||
})
|
||||
# stage values are pairs (id, display_name)
|
||||
cls.ripeness_green = cls.env['x_banana_ripeness'].name_create('Green')
|
||||
cls.ripeness_okay = cls.env['x_banana_ripeness'].name_create('Okay, I guess?')
|
||||
cls.ripeness_gone = cls.env['x_banana_ripeness'].name_create('Walked away on its own')
|
||||
|
||||
# model and records for bananas
|
||||
cls.bananas_model = cls.env['ir.model'].create({
|
||||
'name': 'Bananas',
|
||||
'model': 'x_bananas',
|
||||
'field_id': [
|
||||
Command.create({'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}),
|
||||
Command.create({'name': 'x_length', 'ttype': 'float', 'field_description': 'Length'}),
|
||||
Command.create({'name': 'x_color', 'ttype': 'integer', 'field_description': 'Color'}),
|
||||
Command.create({'name': 'x_ripeness_id', 'ttype': 'many2one',
|
||||
'field_description': 'Ripeness', 'relation': 'x_banana_ripeness',
|
||||
'group_expand': True}),
|
||||
]
|
||||
})
|
||||
# add non-stored field that is not valid in order
|
||||
cls.env['ir.model.fields'].create({
|
||||
'name': 'x_is_yellow',
|
||||
'field_description': 'Is the banana yellow?',
|
||||
'ttype': 'boolean',
|
||||
'model_id': cls.bananas_model.id,
|
||||
'store': False,
|
||||
'depends': 'x_color',
|
||||
'compute': "for banana in self:\n banana['x_is_yellow'] = banana.x_color == 9"
|
||||
})
|
||||
# default stage is ripeness_green
|
||||
cls.env['ir.default'].set('x_bananas', 'x_ripeness_id', cls.ripeness_green[0])
|
||||
cls.env['x_bananas'].create([{
|
||||
'x_name': 'Banana #1',
|
||||
'x_length': 3.14159,
|
||||
'x_color': 9,
|
||||
}, {
|
||||
'x_name': 'Banana #2',
|
||||
'x_length': 0,
|
||||
'x_color': 6,
|
||||
}, {
|
||||
'x_name': 'Banana #3',
|
||||
'x_length': 10,
|
||||
'x_color': 6,
|
||||
}])
|
||||
|
||||
def setUp(self):
|
||||
# this cleanup is necessary after each test, and must be done last
|
||||
self.addCleanup(self.registry.reset_changes)
|
||||
super().setUp()
|
||||
|
||||
def test_model_order_constraint(self):
|
||||
"""Check that the order constraint is properly enforced."""
|
||||
VALID_ORDERS = ['id', 'id desc', 'id asc, x_length', 'x_color, x_length, create_uid']
|
||||
for order in VALID_ORDERS:
|
||||
self.bananas_model.order = order
|
||||
|
||||
INVALID_ORDERS = ['', 'x_wat', 'id esc', 'create_uid,', 'id, x_is_yellow']
|
||||
for order in INVALID_ORDERS:
|
||||
with self.assertRaises(ValidationError):
|
||||
self.bananas_model.order = order
|
||||
|
||||
# check that the constraint is checked at model creation
|
||||
fields_value = [
|
||||
Command.create({'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}),
|
||||
Command.create({'name': 'x_length', 'ttype': 'float', 'field_description': 'Length'}),
|
||||
Command.create({'name': 'x_color', 'ttype': 'integer', 'field_description': 'Color'}),
|
||||
]
|
||||
self.env['ir.model'].create({
|
||||
'name': 'MegaBananas',
|
||||
'model': 'x_mega_bananas',
|
||||
'order': 'x_name asc, id desc', # valid order
|
||||
'field_id': fields_value,
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['ir.model'].create({
|
||||
'name': 'GigaBananas',
|
||||
'model': 'x_giga_bananas',
|
||||
'order': 'x_name asc, x_wat', # invalid order
|
||||
'field_id': fields_value,
|
||||
})
|
||||
|
||||
# ensure we can order by a stored field via inherits
|
||||
user_model = self.env['ir.model'].search([('model', '=', 'res.users')])
|
||||
user_model._check_order() # must not raise
|
||||
|
||||
def test_model_order_search(self):
|
||||
"""Check that custom orders are applied when querying a model."""
|
||||
ORDERS = {
|
||||
'id asc': ['Banana #1', 'Banana #2', 'Banana #3'],
|
||||
'id desc': ['Banana #3', 'Banana #2', 'Banana #1'],
|
||||
'x_color asc, id asc': ['Banana #2', 'Banana #3', 'Banana #1'],
|
||||
'x_color asc, id desc': ['Banana #3', 'Banana #2', 'Banana #1'],
|
||||
'x_length asc, id': ['Banana #2', 'Banana #1', 'Banana #3'],
|
||||
}
|
||||
for order, names in ORDERS.items():
|
||||
self.bananas_model.order = order
|
||||
self.assertEqual(self.env['x_bananas']._order, order)
|
||||
|
||||
bananas = self.env['x_bananas'].search([])
|
||||
self.assertEqual(bananas.mapped('x_name'), names, 'failed to order by %s' % order)
|
||||
|
||||
def test_model_fold_search(self):
|
||||
"""Check that custom orders are applied when querying a model."""
|
||||
self.assertEqual(self.bananas_model.fold_name, False)
|
||||
self.assertEqual(self.env['x_bananas']._fold_name, None)
|
||||
|
||||
self.bananas_model.fold_name = 'x_name'
|
||||
self.assertEqual(self.env['x_bananas']._fold_name, 'x_name')
|
||||
|
||||
def test_group_expansion(self):
|
||||
"""Check that the basic custom group expansion works."""
|
||||
model = self.env['x_bananas'].with_context(read_group_expand=True)
|
||||
groups = model.formatted_read_group([], ['x_ripeness_id'], ['__count'])
|
||||
expected = [{
|
||||
'x_ripeness_id': self.ripeness_green,
|
||||
'__count': 3,
|
||||
'__extra_domain': [('x_ripeness_id', '=', self.ripeness_green[0])],
|
||||
}, {
|
||||
'x_ripeness_id': self.ripeness_okay,
|
||||
'__count': 0,
|
||||
'__extra_domain': [('x_ripeness_id', '=', self.ripeness_okay[0])],
|
||||
}, {
|
||||
'x_ripeness_id': self.ripeness_gone,
|
||||
'__count': 0,
|
||||
'__extra_domain': [('x_ripeness_id', '=', self.ripeness_gone[0])],
|
||||
}]
|
||||
self.assertEqual(groups, expected, 'should include 2 empty ripeness stages')
|
||||
|
||||
def test_rec_name_deletion(self):
|
||||
"""Check that deleting 'x_name' does not crash."""
|
||||
record = self.env['x_bananas'].create({'x_name': "Ifan Ben-Mezd"})
|
||||
self.assertEqual(record._rec_name, 'x_name')
|
||||
ClassRecord = self.registry[record._name]
|
||||
self.assertEqual(self.registry.field_depends[ClassRecord.display_name], ('x_name',))
|
||||
self.assertEqual(record.display_name, "Ifan Ben-Mezd")
|
||||
|
||||
# unlinking x_name should fixup _rec_name and display_name
|
||||
self.env['ir.model.fields']._get('x_bananas', 'x_name').unlink()
|
||||
record = self.env['x_bananas'].browse(record.id)
|
||||
self.assertEqual(record._rec_name, None)
|
||||
self.assertEqual(self.registry.field_depends[ClassRecord.display_name], ())
|
||||
self.assertEqual(record.display_name, f"x_bananas,{record.id}")
|
||||
|
||||
def test_monetary_currency_field(self):
|
||||
fields_value = [
|
||||
Command.create({'name': 'x_monetary', 'ttype': 'monetary', 'field_description': 'Monetary', 'currency_field': 'test'}),
|
||||
]
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['ir.model'].create({
|
||||
'name': 'Paper Company Model',
|
||||
'model': 'x_paper_model',
|
||||
'field_id': fields_value,
|
||||
})
|
||||
|
||||
fields_value = [
|
||||
Command.create({'name': 'x_monetary', 'ttype': 'monetary', 'field_description': 'Monetary', 'currency_field': 'x_falsy_currency'}),
|
||||
Command.create({'name': 'x_falsy_currency', 'ttype': 'one2many', 'field_description': 'Currency', 'relation': 'res.currency'}),
|
||||
]
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['ir.model'].create({
|
||||
'name': 'Paper Company Model',
|
||||
'model': 'x_paper_model',
|
||||
'field_id': fields_value,
|
||||
})
|
||||
|
||||
fields_value = [
|
||||
Command.create({'name': 'x_monetary', 'ttype': 'monetary', 'field_description': 'Monetary', 'currency_field': 'x_falsy_currency'}),
|
||||
Command.create({'name': 'x_falsy_currency', 'ttype': 'many2one', 'field_description': 'Currency', 'relation': 'res.partner'}),
|
||||
]
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['ir.model'].create({
|
||||
'name': 'Paper Company Model',
|
||||
'model': 'x_paper_model',
|
||||
'field_id': fields_value,
|
||||
})
|
||||
|
||||
fields_value = [
|
||||
Command.create({'name': 'x_monetary', 'ttype': 'monetary', 'field_description': 'Monetary', 'currency_field': 'x_good_currency'}),
|
||||
Command.create({'name': 'x_good_currency', 'ttype': 'many2one', 'field_description': 'Currency', 'relation': 'res.currency'}),
|
||||
]
|
||||
model = self.env['ir.model'].create({
|
||||
'name': 'Paper Company Model',
|
||||
'model': 'x_paper_model',
|
||||
'field_id': fields_value,
|
||||
})
|
||||
monetary_field = model.field_id.search([['name', 'ilike', 'x_monetary']])
|
||||
self.assertEqual(len(monetary_field), 1,
|
||||
"Should have the monetary field in the created ir.model")
|
||||
self.assertEqual(monetary_field.currency_field, "x_good_currency",
|
||||
"The currency field in monetary should have x_good_currency as name")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -12,7 +15,7 @@ class TestIrQweb(TransactionCase):
|
|||
})
|
||||
partner = self.env["res.partner"].create({
|
||||
"name": "test image partner",
|
||||
"image_128": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVR4nGJxKFrEwMDAxAAGgAAAAP//D+IBWx9K7TUAAAAASUVORK5CYII=",
|
||||
"image_1920": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAF0lEQVR4nGJxKFrEwMDAxAAGgAAAAP//D+IBWx9K7TUAAAAASUVORK5CYII=",
|
||||
})
|
||||
|
||||
html = view._render_template(view.id, {"is_raw_image": True, "record": partner})
|
||||
|
|
@ -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,33 +96,95 @@ 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=3000, 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"><html><head><meta charset="utf-8"/><link/><script id="web.layout.odooscript"/><meta/><t t-esc="head"/></head><body><t t-out="0"/></body></html></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>'})
|
||||
self.env.ref('web.layout').write({'arch_db': '<t t-name="web.layout"><html><head><meta charset="utf-8"/><link/><script id="web.layout.odooscript"/><meta/><t t-esc="head"/></head><body><t t-out="0"/></body></html></t>'})
|
||||
|
||||
assets = self.env['ir.qweb']._get_asset_content(suite)[0]
|
||||
if len(assets) == 0:
|
||||
|
|
@ -106,19 +192,20 @@ 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=2100, success_signal="[HOOT] Test suite succeeded", error_checker=unit_test_error_checker)
|
||||
|
|
|
|||
|
|
@ -1,49 +1,83 @@
|
|||
from odoo import api, Command
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
|
||||
class LoadMenusTests(HttpCase):
|
||||
|
||||
maxDiff = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.menu = self.env["ir.ui.menu"].create({
|
||||
"name": "test_menu",
|
||||
"name": "root menu (test)",
|
||||
"parent_id": False,
|
||||
})
|
||||
self.action = self.env["ir.actions.act_window"].create({
|
||||
"name": "action (test)",
|
||||
"res_model": "res.users",
|
||||
"view_ids": [Command.create({"view_mode": "form"})],
|
||||
})
|
||||
self.menu_child = self.env["ir.ui.menu"].create({
|
||||
"name": "child menu (test)",
|
||||
"parent_id": self.menu.id,
|
||||
"action": f"{self.action._name},{self.action.id}",
|
||||
})
|
||||
|
||||
def search(*args, **kwargs):
|
||||
return self.menu
|
||||
menus = self.menu + self.menu_child
|
||||
|
||||
self.patch(type(self.env["ir.ui.menu"]), "search", search)
|
||||
# Patch search to only return these menus
|
||||
origin_search_fetch = self.env.registry["ir.ui.menu"].search_fetch
|
||||
|
||||
@api.model
|
||||
def search_fetch(self, domain, *args, **kwargs):
|
||||
return origin_search_fetch(self, domain + [('id', 'in', menus.ids)], *args, **kwargs)
|
||||
|
||||
self.patch(self.env.registry["ir.ui.menu"], "search_fetch", search_fetch)
|
||||
self.authenticate("admin", "admin")
|
||||
|
||||
def test_load_menus(self):
|
||||
menu_loaded = self.url_open("/web/webclient/load_menus/1234")
|
||||
|
||||
menu_loaded = self.url_open("/web/webclient/load_menus")
|
||||
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": ""
|
||||
'actionID': self.action.id, # Take the first action in children (see load_web_menus)
|
||||
'actionModel': 'ir.actions.act_window',
|
||||
'actionPath': False,
|
||||
'appID': self.menu.id,
|
||||
'children': [self.menu_child.id],
|
||||
'id': self.menu.id,
|
||||
'name': 'root menu (test)',
|
||||
'webIcon': False,
|
||||
'webIconData': '/web/static/img/default_icon_app.png',
|
||||
'webIconDataMimetype': False,
|
||||
'xmlid': '',
|
||||
},
|
||||
str(self.menu_child.id): {
|
||||
'actionID': self.action.id,
|
||||
'actionModel': 'ir.actions.act_window',
|
||||
'actionPath': False,
|
||||
'appID': self.menu.id,
|
||||
'children': [],
|
||||
'id': self.menu_child.id,
|
||||
'name': 'child menu (test)',
|
||||
'webIcon': False,
|
||||
'webIconData': False,
|
||||
'webIconDataMimetype': False,
|
||||
'xmlid': '',
|
||||
},
|
||||
'root': {
|
||||
'actionID': False,
|
||||
'actionModel': False,
|
||||
'actionPath': False,
|
||||
'appID': False,
|
||||
'backgroundImage': None,
|
||||
'children': [self.menu.id],
|
||||
'id': 'root',
|
||||
'name': 'root',
|
||||
'webIcon': None,
|
||||
'webIconData': None,
|
||||
'webIconDataMimetype': None,
|
||||
'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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -15,7 +16,7 @@ class TestWebLoginCommon(HttpCase):
|
|||
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 = Opener(self)
|
||||
self.opener.cookies.set('session_id', self.session.sid, domain=HOST, path='/')
|
||||
|
||||
def login(self, username, password, csrf_token=None):
|
||||
|
|
@ -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"
|
||||
})
|
||||
124
odoo-bringout-oca-ocb-web/web/tests/test_partner.py
Normal file
124
odoo-bringout-oca-ocb-web/web/tests/test_partner.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import io
|
||||
import logging
|
||||
import unittest
|
||||
import zipfile
|
||||
from base64 import b64decode
|
||||
|
||||
from odoo.addons.base.tests.common import TransactionCaseWithUserPortal
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.fields import Command
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import vobject
|
||||
except ImportError:
|
||||
_logger.warning("`vobject` Python module not found, vcard file generation disabled. Consider installing this module if you want to generate vcard files")
|
||||
vobject = None
|
||||
|
||||
|
||||
class TestPartnerPrivate(TransactionCaseWithUserPortal):
|
||||
def test_access_onchange(self):
|
||||
partner = self.partner_portal.with_user(self.user_portal)
|
||||
self.assertEqual(partner.has_access('read'), True)
|
||||
self.assertEqual(partner.has_access('write'), False)
|
||||
|
||||
with self.assertRaises(AccessError):
|
||||
partner.onchange({}, ['name'], {'name': {}})
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPartnerVCard(HttpCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
if not vobject:
|
||||
raise unittest.SkipTest("Skip tests when `vobject` Python module is not found.")
|
||||
|
||||
self.partners = self.env['res.partner'].create([{
|
||||
'name': 'John Doe',
|
||||
'email': 'john.doe@test.example.com',
|
||||
'phone': '+1 202 555 0122',
|
||||
'function': 'Painter',
|
||||
'street': 'Cookieville Minimum-Security Orphanarium',
|
||||
'city': 'New York',
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
'zip': '97648',
|
||||
'website': 'https://test.exemple.com',
|
||||
}, {
|
||||
'name': 'shut',
|
||||
'email': 'shut@test.example.com',
|
||||
'phone': '+1 202 555 0123',
|
||||
'function': 'Developer',
|
||||
'street': 'Donutville Maximum-Security Orphanarium',
|
||||
'city': 'Washington DC',
|
||||
'country_id': self.env.ref('base.us').id,
|
||||
'zip': '97649',
|
||||
'website': 'https://test.example.com',
|
||||
'child_ids': [
|
||||
Command.create({'type': 'other'})
|
||||
]
|
||||
}])
|
||||
self.authenticate("admin", "admin")
|
||||
|
||||
def check_vcard_contents(self, vcard, partner):
|
||||
self.assertEqual(vcard.contents["n"][0].value.family, partner.name, "Vcard should have the same name")
|
||||
self.assertEqual(vcard.contents["adr"][0].value.street, partner.street, "Vcard should have the same street")
|
||||
self.assertEqual(vcard.contents["adr"][0].value.city, partner.city, "Vcard should have the same city")
|
||||
self.assertEqual(vcard.contents["adr"][0].value.code, partner.zip, "Vcard should have the same zip")
|
||||
self.assertEqual(vcard.contents["adr"][0].value.country, self.env.ref('base.us').name, "Vcard should have the same country")
|
||||
self.assertEqual(vcard.contents["email"][0].value, partner.email, "Vcard should have the same email")
|
||||
self.assertEqual(vcard.contents["url"][0].value, partner.website, "Vcard should have the same website")
|
||||
self.assertEqual(vcard.contents["tel"][0].params['TYPE'], ["work"], "Vcard should have the same phone")
|
||||
self.assertEqual(vcard.contents["tel"][0].value, partner.phone, "Vcard should have the same phone")
|
||||
self.assertEqual(vcard.contents["title"][0].value, partner.function, "Vcard should have the same function")
|
||||
self.assertEqual(len(vcard.contents['photo'][0].value), len(b64decode(partner.avatar_512)), "Vcard should have the same photo")
|
||||
|
||||
def test_fetch_single_partner_vcard(self):
|
||||
res = self.url_open('/web_enterprise/partner/%d/vcard' % self.partners[0].id)
|
||||
vcard = vobject.readOne(res.text)
|
||||
self.check_vcard_contents(vcard, self.partners[0])
|
||||
|
||||
def test_fetch_multiple_partners_vcard(self):
|
||||
res = self.url_open('/web/partner/vcard?partner_ids=%s,%s'
|
||||
% (self.partners[0].id, self.partners[1].id))
|
||||
with io.BytesIO(res.content) as buffer:
|
||||
with zipfile.ZipFile(buffer, 'r') as zipf:
|
||||
vcfFileList = zipf.namelist()
|
||||
for i, vcfFile in enumerate(vcfFileList):
|
||||
vcardFile = zipf.read(vcfFile).decode()
|
||||
self.check_vcard_contents(vobject.readOne(vcardFile), self.partners[i])
|
||||
|
||||
@unittest.skip
|
||||
def test_not_exist_partner_vcard(self):
|
||||
partner_id = self.partner.id
|
||||
self.partner.unlink()
|
||||
res = self.url_open('/web/partner/%d/vcard' % partner_id)
|
||||
self.assertEqual(res.status_code, 404)
|
||||
|
||||
def test_check_partner_access_for_user(self):
|
||||
self.env['res.users'].create({
|
||||
'group_ids': [Command.set([self.env.ref('base.group_public').id])],
|
||||
'name': 'Test User',
|
||||
'login': 'testuser',
|
||||
'password': 'testuser',
|
||||
})
|
||||
self.authenticate('testuser', 'testuser')
|
||||
with mute_logger('odoo.http'): # mute 403 warning
|
||||
res = self.url_open('/web/partner/vcard?partner_ids=%s,%s' %
|
||||
(self.partners[0].id, self.partners[1].id))
|
||||
self.assertEqual(res.status_code, 403)
|
||||
|
||||
def test_fetch_single_partner_vcard_without_name(self):
|
||||
"""
|
||||
Test to fetch a vcard of a partner create through
|
||||
child of another partner without name
|
||||
"""
|
||||
partner = self.partners[1].child_ids[0]
|
||||
res = self.url_open('/web/partner/vcard?partner_ids=%s' % partner.id)
|
||||
vcard = vobject.readOne(res.text)
|
||||
self.assertEqual(vcard.contents["n"][0].value.family, partner.complete_name, "Vcard will have the complete name when it dosen't have name")
|
||||
98
odoo-bringout-oca-ocb-web/web/tests/test_perf_load_menu.py
Normal file
98
odoo-bringout-oca-ocb-web/web/tests/test_perf_load_menu.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
|
||||
import json
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo.tests import common, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestPerfSessionInfo(common.HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Improve stability of query count by using dedicated company and user.
|
||||
cls.company = cls.env['res.company'].create({
|
||||
'name': 'Test Company',
|
||||
})
|
||||
cls.user = common.new_test_user(
|
||||
cls.env,
|
||||
"session",
|
||||
email="session@in.fo",
|
||||
tz="UTC",
|
||||
company_id=cls.company.id,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.uid = self.user
|
||||
|
||||
def test_performance_session_info(self):
|
||||
self.authenticate(self.user.login, "info")
|
||||
|
||||
self.env.registry.clear_all_caches()
|
||||
# cold ormcache:
|
||||
# - Only web: 43
|
||||
# - All modules: 121
|
||||
with self.assertQueryCount(121):
|
||||
self.url_open(
|
||||
"/web/session/get_session_info",
|
||||
data=json.dumps({'jsonrpc': "2.0", 'method': "call", 'id': str(uuid4())}),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
# cold fields cache - warm ormcache:
|
||||
# - Only web: 5
|
||||
# - All modules: 32
|
||||
with self.assertQueryCount(32):
|
||||
self.url_open(
|
||||
"/web/session/get_session_info",
|
||||
data=json.dumps({'jsonrpc': "2.0", 'method': "call", 'id': str(uuid4())}),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
def test_load_web_menus_perf(self):
|
||||
self.env.registry.clear_all_caches()
|
||||
self.env.invalidate_all()
|
||||
# cold orm/fields cache:
|
||||
# - Web only: 14
|
||||
# - All modules 57
|
||||
with self.assertQueryCount(57):
|
||||
self.env['ir.ui.menu'].load_web_menus(False)
|
||||
|
||||
# cold fields cache:
|
||||
# - Web only: 0
|
||||
# - All modules: 1 (web_studio + 1)
|
||||
self.env.invalidate_all()
|
||||
with self.assertQueryCount(1):
|
||||
self.env['ir.ui.menu'].load_web_menus(False)
|
||||
|
||||
def test_load_menus_perf(self):
|
||||
self.env.registry.clear_all_caches()
|
||||
self.env.invalidate_all()
|
||||
# cold orm/fields cache:
|
||||
# - Web only: 14
|
||||
# - All modules 57
|
||||
with self.assertQueryCount(57):
|
||||
self.env['ir.ui.menu'].load_menus(False)
|
||||
|
||||
# cold fields cache:
|
||||
# - Web only: 0
|
||||
# - All modules: 1 (web_studio + 1)
|
||||
self.env.invalidate_all()
|
||||
with self.assertQueryCount(1):
|
||||
self.env['ir.ui.menu'].load_menus(False)
|
||||
|
||||
def test_visible_menu_ids(self):
|
||||
self.env.registry.clear_all_caches()
|
||||
self.env.invalidate_all()
|
||||
# cold ormcache:
|
||||
# - Only web 13
|
||||
# - All modules: 21
|
||||
with self.assertQueryCount(21):
|
||||
self.env['ir.ui.menu']._visible_menu_ids()
|
||||
|
||||
# cold fields cache - warm orm cache (only web: 0, all module: 0)
|
||||
self.env.invalidate_all()
|
||||
with self.assertQueryCount(0):
|
||||
self.env['ir.ui.menu']._visible_menu_ids()
|
||||
50
odoo-bringout-oca-ocb-web/web/tests/test_pivot_export.py
Normal file
50
odoo-bringout-oca-ocb-web/web/tests/test_pivot_export.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import io
|
||||
import json
|
||||
from lxml import etree
|
||||
from zipfile import ZipFile
|
||||
|
||||
from odoo import http
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
|
||||
class TestPivotExport(HttpCase):
|
||||
|
||||
def test_export_xlsx_with_integer_column(self):
|
||||
""" Test the export_xlsx method of the pivot controller with int columns """
|
||||
self.authenticate('admin', 'admin')
|
||||
jdata = {
|
||||
'title': 'Sales Analysis',
|
||||
'model': 'sale.report',
|
||||
'measure_count': 1,
|
||||
'origin_count': 1,
|
||||
'col_group_headers': [
|
||||
[{'title': 500, 'width': 1, 'height': 1}],
|
||||
],
|
||||
'measure_headers': [],
|
||||
'origin_headers': [],
|
||||
'rows': [
|
||||
{'title': 1, 'indent': 0, 'values': [{'value': 42}]},
|
||||
],
|
||||
}
|
||||
response = self.url_open(
|
||||
'/web/pivot/export_xlsx',
|
||||
data={
|
||||
'data': json.dumps(jdata),
|
||||
'csrf_token': http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
zip_file = ZipFile(io.BytesIO(response.content))
|
||||
|
||||
with zip_file.open('xl/worksheets/sheet1.xml') as file:
|
||||
sheet_tree = etree.parse(file)
|
||||
xml_data = {}
|
||||
|
||||
for c in sheet_tree.iterfind('.//{http://schemas.openxmlformats.org/spreadsheetml/2006/main}c'):
|
||||
cell_ref = c.attrib['r']
|
||||
value = c.findtext('{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v')
|
||||
xml_data[cell_ref] = value
|
||||
|
||||
self.assertEqual(xml_data['B1'], '500')
|
||||
self.assertEqual(xml_data['A2'], '0')
|
||||
self.assertEqual(xml_data['B2'], '42')
|
||||
|
|
@ -55,10 +55,10 @@ class TestProfilingWeb(ProfilingHttpCase):
|
|||
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
|
||||
res = self.url_open(f'/web/login') # 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?')
|
||||
self.assertEqual(new_profile.name, f'/web/login?')
|
||||
|
||||
def test_profile_test_tool(self):
|
||||
with self.profile():
|
||||
|
|
@ -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},
|
||||
})
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
import os
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import odoo.tests
|
||||
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
from odoo.addons.http_routing.tests.common import MockRequest
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import root
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestReports(odoo.tests.HttpCase):
|
||||
|
|
@ -37,23 +43,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,12 +75,66 @@ 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'
|
||||
)
|
||||
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')
|
||||
|
||||
@mute_logger('odoo.addons.base.models.ir_actions_report')
|
||||
def test_report_error_cleanup(self):
|
||||
admin = self.env.ref('base.user_admin')
|
||||
self.env['ir.ui.view'].create({
|
||||
'type': 'qweb',
|
||||
'name': 'base.test_report',
|
||||
'key': 'base.test_report',
|
||||
'arch': '''
|
||||
<main>
|
||||
<div">
|
||||
<p>TEST</p>
|
||||
</div>
|
||||
</main>
|
||||
'''
|
||||
})
|
||||
report = self.env['ir.actions.report'].create({
|
||||
'name': 'test report',
|
||||
'report_name': 'base.test_report',
|
||||
'model': 'res.partner',
|
||||
})
|
||||
report = report.with_user(admin)
|
||||
|
||||
with (MockRequest(report.env) as mock_request,
|
||||
patch('subprocess.run') as mock_popen,
|
||||
patch.object(root.session_store, 'delete') as mock_delete,
|
||||
patch.object(os, 'unlink') as mock_unlink):
|
||||
|
||||
mock_request.session = self.authenticate(admin.login, admin.login)
|
||||
|
||||
mock_process = Mock()
|
||||
mock_process.returncode = -1
|
||||
mock_process.stderr = ""
|
||||
mock_popen.return_value = mock_process
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
report.with_context(force_report_rendering=True)._render_qweb_pdf(report.id)
|
||||
|
||||
# Check if the temporary session has been deleted
|
||||
self.assertEqual(mock_delete.call_count, 1)
|
||||
self.assertNotEqual(mock_delete.call_args.args[0].sid, mock_request.session.sid)
|
||||
|
||||
# Check if temporary files have been deleted
|
||||
deleted_files = ''.join([call.args[0] for call in mock_unlink.call_args_list])
|
||||
self.assertIn('report.cookie_jar.tmp', deleted_files)
|
||||
self.assertIn('report.header.tmp', deleted_files)
|
||||
self.assertIn('report.footer.tmp', deleted_files)
|
||||
self.assertIn('report.body.tmp', deleted_files)
|
||||
self.assertIn('report.tmp', deleted_files)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import TransactionCase
|
||||
|
||||
|
||||
class TestWebProperties(TransactionCase):
|
||||
def test_get_properties_base_definition(self):
|
||||
"""Check that we can not get the base definition if we can not read the model."""
|
||||
self.env["properties.base.definition"].get_properties_base_definition("res.partner", "properties")
|
||||
|
||||
def _check_access(mode):
|
||||
if mode == "read":
|
||||
raise AccessError("")
|
||||
|
||||
with patch.object(self.registry['res.partner'], 'check_access', side_effect=_check_access), \
|
||||
self.assertRaises(AccessError):
|
||||
self.env["properties.base.definition"].get_properties_base_definition("res.partner", "properties")
|
||||
69
odoo-bringout-oca-ocb-web/web/tests/test_res_users.py
Normal file
69
odoo-bringout-oca-ocb-web/web/tests/test_res_users.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form, TransactionCase
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
class TestResUsers(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.users = cls.env["res.users"].create([
|
||||
{'name': 'Jean', 'login': 'jean@mail.com', 'password': 'jean@mail.com'},
|
||||
{'name': 'Jean-Paul', 'login': 'jean-paul@mail.com', 'password': 'jean-paul@mail.com'},
|
||||
{'name': 'Jean-Jacques', 'login': 'jean-jacques@mail.com', 'password': 'jean-jacques@mail.com'},
|
||||
{'name': 'Georges', 'login': 'georges@mail.com', 'password': 'georges@mail.com'},
|
||||
{'name': 'Claude', 'login': 'claude@mail.com', 'password': 'claude@mail.com'},
|
||||
{'name': 'Pascal', 'login': 'pascal@mail.com', 'password': 'pascal@mail.com'},
|
||||
])
|
||||
|
||||
def test_name_search(self):
|
||||
"""
|
||||
Test name search with self assign feature
|
||||
The self assign feature is present only when a limit is present,
|
||||
which is the case with the public name_search by default
|
||||
"""
|
||||
ResUsers = self.env['res.users']
|
||||
jean = self.users[0]
|
||||
user_ids = [id_ for id_, __ in ResUsers.with_user(jean).name_search('')]
|
||||
self.assertEqual(jean.id, user_ids[0], "The current user, Jean, should be the first in the result.")
|
||||
user_ids = [id_ for id_, __ in ResUsers.with_user(jean).name_search('Claude')]
|
||||
self.assertNotIn(jean.id, user_ids, "The current user, Jean, should not be in the result because his name does not fit the condition.")
|
||||
pascal = self.users[-1]
|
||||
user_ids = [id_ for id_, __ in ResUsers.with_user(pascal).name_search('')]
|
||||
self.assertEqual(pascal.id, user_ids[0], "The current user, Pascal, should be the first in the result.")
|
||||
user_ids = [id_ for id_, __ in ResUsers.with_user(pascal).name_search('', limit=3)]
|
||||
self.assertEqual(pascal.id, user_ids[0], "The current user, Pascal, should be the first in the result.")
|
||||
self.assertEqual(len(user_ids), 3, "The number of results found should still respect the limit set.")
|
||||
jean_paul = self.users[1]
|
||||
user_ids = [id_ for id_, __ in ResUsers.with_user(jean_paul).name_search('Jean')]
|
||||
self.assertEqual(jean_paul.id, user_ids[0], "The current user, Jean-Paul, should be the first in the result")
|
||||
claude = self.users[4]
|
||||
user_ids = [id_ for id_, __ in ResUsers.with_user(claude).name_search('', limit=2)]
|
||||
self.assertEqual(claude.id, user_ids[0], "The current user, Claude, should be the first in the result.")
|
||||
self.assertNotEqual(claude.id, user_ids[1], "The current user, Claude, should not appear twice in the result")
|
||||
user_ids = [id_ for id_, __ in ResUsers.with_user(claude).name_search('', limit=5)]
|
||||
self.assertEqual(len(user_ids), len(set(user_ids)), "Some user(s), appear multiple times in the result")
|
||||
|
||||
def test_change_password(self):
|
||||
'''
|
||||
We should be able to change user password without any issue
|
||||
'''
|
||||
user_internal = self.env['res.users'].create({
|
||||
'name': 'Internal',
|
||||
'login': 'user_internal',
|
||||
'password': 'password',
|
||||
'group_ids': [self.env.ref('base.group_user').id],
|
||||
})
|
||||
with Form(self.env['change.password.wizard'].with_context(active_model='res.users', active_ids=user_internal.ids), view='base.change_password_wizard_view') as form:
|
||||
with form.user_ids.edit(0) as line:
|
||||
line.new_passwd = 'bla'
|
||||
rec = form.save()
|
||||
rec.change_password_button()
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestUserSettings(HttpCaseWithUserDemo):
|
||||
def test_user_group_settings(self):
|
||||
self.start_tour('/odoo?debug=1', 'test_user_group_settings', login='admin')
|
||||
84
odoo-bringout-oca-ocb-web/web/tests/test_router.py
Normal file
84
odoo-bringout-oca-ocb-web/web/tests/test_router.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from odoo.tests.common import TransactionCase
|
||||
from odoo.addons.web.controllers.utils import get_action_triples, get_action
|
||||
|
||||
|
||||
class TestWebRouter(TransactionCase):
|
||||
def test_router_get_action_exist(self):
|
||||
ir_cron_act = self.env.ref('base.ir_cron_act')
|
||||
valid_actions = [
|
||||
f'action-{ir_cron_act.id}', # record id
|
||||
'action-base.ir_cron_act', # xml id
|
||||
'm-ir.cron', # m- model name (for website)
|
||||
'ir.cron', # dotted model name
|
||||
'crons', # action path
|
||||
]
|
||||
for action in valid_actions:
|
||||
with self.subTest(action=action):
|
||||
self.assertEqual(get_action(self.env, action), ir_cron_act)
|
||||
|
||||
def test_router_get_action_missing(self):
|
||||
Actions = self.env['ir.actions.actions']
|
||||
missing_actions = [
|
||||
'action-999999999',
|
||||
'action-base.idontexist',
|
||||
'm-base', # abstract model
|
||||
'm-idontexist',
|
||||
'base.idontexist',
|
||||
'idontexist',
|
||||
]
|
||||
for action in missing_actions:
|
||||
with self.subTest(action=action):
|
||||
self.assertEqual(get_action(self.env, action), Actions)
|
||||
|
||||
def test_router_get_action_triples_exist(self):
|
||||
base = self.env['ir.module.module'].search([('name', '=', 'base')])
|
||||
user = self.env.user
|
||||
ir_cron_act = self.env.ref('base.ir_cron_act')
|
||||
|
||||
matrix = {
|
||||
# single action
|
||||
f'action-{ir_cron_act.id}': [(None, ir_cron_act, None)],
|
||||
'action-base.ir_cron_act': [(None, ir_cron_act, None)],
|
||||
'm-ir.cron': [(None, ir_cron_act, None)],
|
||||
'ir.cron': [(None, ir_cron_act, None)],
|
||||
'crons': [(None, ir_cron_act, None)],
|
||||
|
||||
# multiple actions, all are accessible by clicking in the web client
|
||||
|
||||
# Apps > Base > Module info
|
||||
f'apps/{base.id}/ir.module.module/{base.id}': [
|
||||
(None, self.env.ref('base.open_module_tree'), base.id),
|
||||
(base.id, self.env.ref('base.open_module_tree'), base.id)],
|
||||
|
||||
# Settings > Users & Companies > Users > Marc Demo > Related Partner
|
||||
f'users/{user.id}/res.partner/{user.partner_id.id}': [
|
||||
(None, self.env.ref('base.action_res_users'), user.id),
|
||||
(user.id, self.env.ref('base.action_partner_form'), user.partner_id.id)],
|
||||
|
||||
# Settings > Users & Companies > Users > Marc Demo > Access Right > TOTP
|
||||
f'users/{user.id}/ir.model.access/ir.model.access/146': [
|
||||
(None, self.env.ref('base.action_res_users'), user.id),
|
||||
(user.id, self.env.ref('base.ir_access_act'), None),
|
||||
(user.id, self.env.ref('base.ir_access_act'), 146),
|
||||
]
|
||||
}
|
||||
for path, triples in matrix.items():
|
||||
with self.subTest(path=path):
|
||||
self.assertEqual(list(get_action_triples(self.env, path)), triples)
|
||||
|
||||
def test_router_get_action_triples_missing(self):
|
||||
# single unknown action
|
||||
missing_actions = [
|
||||
'action-999999999',
|
||||
'action-base.idontexist',
|
||||
'm-base',
|
||||
'm-idontexist',
|
||||
'base.idontexist',
|
||||
'idontexist',
|
||||
]
|
||||
for action in missing_actions:
|
||||
with self.subTest(path=action):
|
||||
with self.assertRaises(ValueError) as capture:
|
||||
all(get_action_triples(self.env, action))
|
||||
self.assertEqual(capture.exception.args[0],
|
||||
f"expected action at word 0 but found “{action}”")
|
||||
|
|
@ -14,7 +14,9 @@ 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.allowed_companies = cls.company_a + cls.company_b_branch + cls.company_c
|
||||
cls.disallowed_ancestor_companies = cls.company_b
|
||||
|
||||
cls.user_password = "info"
|
||||
cls.user = common.new_test_user(
|
||||
|
|
@ -25,7 +27,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 +38,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,12 +52,30 @@ class TestSessionInfo(common.HttpCase):
|
|||
'id': company.id,
|
||||
'name': company.name,
|
||||
'sequence': company.sequence,
|
||||
} for company in self.companies
|
||||
'child_ids': company.child_ids.ids,
|
||||
'currency_id': company.currency_id.id,
|
||||
'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["groups"], {
|
||||
'base.group_allow_export': self.user.has_group('base.group_allow_export')
|
||||
})
|
||||
self.assertEqual(
|
||||
result['user_companies'],
|
||||
expected_user_companies,
|
||||
|
|
@ -63,12 +86,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()
|
||||
|
|
|
|||
42
odoo-bringout-oca-ocb-web/web/tests/test_translate.py
Normal file
42
odoo-bringout-oca-ocb-web/web/tests/test_translate.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
class TestTranslationOverride(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.category = cls.env['res.partner.category'].create({'name': 'Reblochon'})
|
||||
cls.custom = cls.env['ir.model.fields'].create({
|
||||
'name': 'x_html_test',
|
||||
'ttype': 'html',
|
||||
'model_id': cls.category.id,
|
||||
'translate': 'html_translate',
|
||||
})
|
||||
|
||||
def test_web_override_translations(self):
|
||||
self.env['res.lang']._activate_lang('fr_FR')
|
||||
categoryEN = self.category.with_context(lang='en_US')
|
||||
categoryFR = self.category.with_context(lang='fr_FR')
|
||||
customEN = self.custom.with_context(lang='en_US')
|
||||
customFR = self.custom.with_context(lang='fr_FR')
|
||||
|
||||
self.category.web_override_translations({'name': 'commonName'})
|
||||
self.assertEqual(categoryEN.name, 'commonName')
|
||||
self.assertEqual(categoryFR.name, 'commonName')
|
||||
|
||||
# cannot void translations (incluiding en_US)
|
||||
self.category.web_override_translations({'name': False})
|
||||
self.assertEqual(categoryEN.name, 'commonName')
|
||||
self.assertEqual(categoryFR.name, 'commonName')
|
||||
|
||||
# empty str is a valid translation
|
||||
self.category.web_override_translations({'name': ''})
|
||||
self.assertEqual(categoryEN.name, '')
|
||||
self.assertEqual(categoryFR.name, '')
|
||||
|
||||
# translated html fields are not changed
|
||||
self.custom.web_override_translations({'name': '<div>dont</div><div>change</div>'})
|
||||
self.assertEqual(customEN.name, 'x_html_test')
|
||||
self.assertEqual(customFR.name, 'x_html_test')
|
||||
26
odoo-bringout-oca-ocb-web/web/tests/test_web_redirect.py
Normal file
26
odoo-bringout-oca-ocb-web/web/tests/test_web_redirect.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.urls import url_parse
|
||||
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
|
||||
class TestWebRedirect(HttpCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def test_web_route_redirect_param_legacy(self):
|
||||
# This test is for legacy routes with /web and fragement
|
||||
web_response = self.url_open('/web#cids=1&action=887&menu_id=124')
|
||||
web_response.raise_for_status()
|
||||
response_url_query = url_parse(web_response.url).query
|
||||
|
||||
self.assertEqual(response_url_query, 'redirect=%2Fweb%3F')
|
||||
|
||||
def test_web_route_redirect_param(self):
|
||||
# This test if for the new routes with /odoo, pathname and query params
|
||||
web_response = self.url_open('/odoo/action-887?cids=1')
|
||||
web_response.raise_for_status()
|
||||
response_url_query = url_parse(web_response.url).query
|
||||
|
||||
self.assertEqual(response_url_query, 'redirect=%2Fodoo%2Faction-887%3Fcids%3D1')
|
||||
|
|
@ -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]
|
||||
|
||||
|
|
@ -20,17 +21,22 @@ class TestWebSearchRead(common.TransactionCase):
|
|||
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)
|
||||
with patch('odoo.addons.base.models.res_currency.ResCurrency.search_count', new=search_count):
|
||||
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)
|
||||
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)
|
||||
|
||||
def test_web_name_search(self):
|
||||
result = self.env["res.partner"].web_name_search("", {"display_name": {}})[0]
|
||||
self.assertTrue("display_name" in result)
|
||||
self.assertTrue("__formatted_display_name" in result)
|
||||
|
|
|
|||
109
odoo-bringout-oca-ocb-web/web/tests/test_webmanifest.py
Normal file
109
odoo-bringout-oca-ocb-web/web/tests/test_webmanifest.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class WebManifestRoutesTest(HttpCaseWithUserDemo):
|
||||
"""
|
||||
This test suite is used to request the routes used by the PWA backend implementation
|
||||
"""
|
||||
|
||||
def test_webmanifest(self):
|
||||
"""
|
||||
This route returns a well formed backend's WebManifest
|
||||
"""
|
||||
self.authenticate("admin", "admin")
|
||||
response = self.url_open("/web/manifest.webmanifest")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "application/manifest+json")
|
||||
data = response.json()
|
||||
self.assertEqual(data["name"], "Odoo")
|
||||
self.assertEqual(data["scope"], "/odoo")
|
||||
self.assertEqual(data["start_url"], "/odoo")
|
||||
self.assertEqual(data["display"], "standalone")
|
||||
self.assertEqual(data["background_color"], "#714B67")
|
||||
self.assertEqual(data["theme_color"], "#714B67")
|
||||
self.assertEqual(data["prefer_related_applications"], False)
|
||||
self.assertCountEqual(data["icons"], [
|
||||
{'src': '/web/static/img/odoo-icon-192x192.png', 'sizes': '192x192', 'type': 'image/png'},
|
||||
{'src': '/web/static/img/odoo-icon-512x512.png', 'sizes': '512x512', 'type': 'image/png'}
|
||||
])
|
||||
self.assertGreaterEqual(len(data["shortcuts"]), 0)
|
||||
for shortcut in data["shortcuts"]:
|
||||
self.assertGreater(len(shortcut["name"]), 0)
|
||||
self.assertGreater(len(shortcut["description"]), 0)
|
||||
self.assertGreater(len(shortcut["icons"]), 0)
|
||||
self.assertTrue(shortcut["url"].startswith("/odoo?menu_id="))
|
||||
|
||||
def test_webmanifest_unauthenticated(self):
|
||||
"""
|
||||
This route returns a well formed backend's WebManifest
|
||||
"""
|
||||
response = self.url_open("/web/manifest.webmanifest")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "application/manifest+json")
|
||||
data = response.json()
|
||||
self.assertEqual(data["name"], "Odoo")
|
||||
self.assertEqual(data["scope"], "/odoo")
|
||||
self.assertEqual(data["start_url"], "/odoo")
|
||||
self.assertEqual(data["display"], "standalone")
|
||||
self.assertEqual(data["background_color"], "#714B67")
|
||||
self.assertEqual(data["theme_color"], "#714B67")
|
||||
self.assertEqual(data["prefer_related_applications"], False)
|
||||
self.assertCountEqual(data["icons"], [
|
||||
{'src': '/web/static/img/odoo-icon-192x192.png', 'sizes': '192x192', 'type': 'image/png'},
|
||||
{'src': '/web/static/img/odoo-icon-512x512.png', 'sizes': '512x512', 'type': 'image/png'}
|
||||
])
|
||||
self.assertEqual(len(data["shortcuts"]), 0)
|
||||
|
||||
def test_webmanifest_scoped(self):
|
||||
response = self.url_open("/web/manifest.scoped_app_manifest?app_id=test&path=/test&app_name=Test")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "application/manifest+json")
|
||||
data = response.json()
|
||||
self.assertEqual(data["name"], "Test")
|
||||
self.assertEqual(data["scope"], "/test")
|
||||
self.assertEqual(data["start_url"], "/test")
|
||||
self.assertEqual(data["display"], "standalone")
|
||||
self.assertEqual(data["background_color"], "#714B67")
|
||||
self.assertEqual(data["theme_color"], "#714B67")
|
||||
self.assertEqual(data["prefer_related_applications"], False)
|
||||
self.assertCountEqual(data["icons"], [
|
||||
{'src': "/web/static/img/odoo-icon-192x192.png", 'sizes': 'any', 'type': 'image/png'}
|
||||
])
|
||||
self.assertEqual(len(data["shortcuts"]), 0)
|
||||
|
||||
def test_serviceworker(self):
|
||||
"""
|
||||
This route returns a JavaScript's ServiceWorker
|
||||
"""
|
||||
response = self.url_open("/web/service-worker.js")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "text/javascript")
|
||||
self.assertEqual(response.headers["Service-Worker-Allowed"], "/odoo")
|
||||
|
||||
def test_offline_url(self):
|
||||
"""
|
||||
This route returns the offline page
|
||||
"""
|
||||
response = self.url_open("/odoo/offline")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.headers["Content-Type"], "text/html; charset=utf-8")
|
||||
|
||||
def test_apple_touch_icon(self):
|
||||
"""
|
||||
This request tests the presence of an apple-touch-icon image route for the PWA icon and
|
||||
its presence from the head of the document.
|
||||
"""
|
||||
self.authenticate("demo", "demo")
|
||||
response = self.url_open("/web/static/img/odoo-icon-ios.png")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
document = self.url_open("/odoo")
|
||||
self.assertIn(
|
||||
'<link rel="apple-touch-icon" href="/web/static/img/odoo-icon-ios.png"/>', document.text,
|
||||
"Icon for iOS is present in the head of the document.",
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue