From 2fa9dd0ad91b6dad4fbb2f81e018f432ec1efc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Faure-Lacroix?= Date: Wed, 1 Feb 2023 19:15:02 -0500 Subject: [PATCH] Added utility to export assets manually - Added tests for utility --- odoo_tools/cli/click/manage.py | 33 +++++++++++ odoo_tools/modules/assets.py | 68 +++++++++++++++++++++ tests/cli/test_manage.py | 45 ++++++++++++++ tests/modules/test_assets.py | 104 +++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+) create mode 100644 odoo_tools/modules/assets.py create mode 100644 tests/cli/test_manage.py create mode 100644 tests/modules/test_assets.py diff --git a/odoo_tools/cli/click/manage.py b/odoo_tools/cli/click/manage.py index c89e760..c0c91f9 100644 --- a/odoo_tools/cli/click/manage.py +++ b/odoo_tools/cli/click/manage.py @@ -1,9 +1,11 @@ +import re import click import logging from pathlib import Path from .utils import MODULE_TYPE from ...configuration.misc import DictObject +from ...modules.assets import AssetsBundler _logger = logging.getLogger(__name__) @@ -48,6 +50,37 @@ def update(ctx, database, modules): return True +@manage.command( + help="Build Asset" +) +@click.option( + '-d', + '--database', + help="Database" +) +@click.option( + '--minified', + is_flag=True, + default=False +) +@click.argument("type") +@click.argument("asset") +@click.pass_context +def asset(ctx, database, asset, type, minified): + env = ctx.obj['env'] + env.check_odoo() + manage = env.manage.db(database) + manage.default_entrypoints() + + with manage.env() as oenv: + bundler = AssetsBundler(oenv, asset) + + if type == 'css': + print(bundler.get_css(minified)) + elif type == 'js': + print(bundler.get_js(minified)) + + @manage.command( help="Install modules in the provided database" ) diff --git a/odoo_tools/modules/assets.py b/odoo_tools/modules/assets.py new file mode 100644 index 0000000..17795c6 --- /dev/null +++ b/odoo_tools/modules/assets.py @@ -0,0 +1,68 @@ +import re + + +class AssetsBundler(object): + def __init__(self, env, asset): + self.env = env + self.asset = asset + self._bundle = None + + @property + def bundle(self): + if not self._bundle: + self._bundle = self.get_bundle() + + return self._bundle + + def get_bundle(self): + from odoo.addons.base.models.assetsbundle import AssetsBundle + self.files = self.get_files() + return AssetsBundle(self.asset, self.files[0], env=self.env) + + def get_files(self): + qweb = self.env['ir.qweb'] + files = qweb._get_asset_content(self.asset) + return files + + def get_js(self, minified=False): + result = [] + + if minified: + for js in self.bundle.javascripts: + result.append(f"{js.minify()};") + else: + for js in self.bundle.javascripts: + result.append(js.with_header(js.content, minimal=False)) + + return "\n".join(result) + + def get_css(self, minified=False): + from odoo.addons.base.models.assetsbundle import AssetsBundle + + data = self.bundle.preprocess_css() + + matches = [] + data = re.sub( + AssetsBundle.rx_css_import, + lambda matchobj: matches.append(matchobj.group(0)) and '', + data + ) + + if minified: + matches.append(data) + else: + for style in self.bundle.stylesheets: + if not style.content: + continue + + content = style.with_header(style.content) + + content = re.sub( + AssetsBundle.rx_css_import, + lambda matchobj: f"/* {matchobj.group(0)} */", + content + ) + + matches.append(content) + + return "\n".join(matches) diff --git a/tests/cli/test_manage.py b/tests/cli/test_manage.py new file mode 100644 index 0000000..059b291 --- /dev/null +++ b/tests/cli/test_manage.py @@ -0,0 +1,45 @@ +from mock import patch, MagicMock +from odoo_tools.cli.odot import command +from odoo_tools.api.environment import Environment + + +def test_bundler(runner): + + def fake_check_odoo(self): + self.manage = MagicMock() + + obj_path = 'odoo_tools.cli.click.manage.AssetsBundler' + + with patch(obj_path) as bundler, \ + patch.object(Environment, 'check_odoo', autospec=True) as check_odoo: + + bun_instance = MagicMock() + bundler.return_value = bun_instance + + check_odoo.side_effect = fake_check_odoo + # manage.return_value = MagicMock() + + result = runner.invoke( + command, + [ + 'manage', + 'asset', + 'css', + 'base.common', + ] + ) + + assert result.exception is None + bun_instance.get_css.assert_called_once() + bun_instance.get_js.assert_not_called() + + result = runner.invoke( + command, + [ + 'manage', + 'asset', + 'js', + 'base.common', + ] + ) + bun_instance.get_js.assert_called_once() diff --git a/tests/modules/test_assets.py b/tests/modules/test_assets.py new file mode 100644 index 0000000..e037c48 --- /dev/null +++ b/tests/modules/test_assets.py @@ -0,0 +1,104 @@ +import re + +import pytest +from mock import MagicMock, patch + +from odoo_tools.modules.assets import AssetsBundler + + +@pytest.fixture +def modules(): + odoo = MagicMock() + + models = odoo.addons.base.models + + return { + "odoo": odoo, + "odoo.addons": odoo.addons, + "odoo.addons.base": odoo.addons.base, + "odoo.addons.base.models": odoo.addons.base.models, + "odoo.addons.base.models.assetsbundle": models.assetsbundle, + } + + +class MockAsset(object): + def __init__(self, name, data): + self.name = name + self.content = data + + def with_header(self, content, minimal=False): + return f"/* {self.name} */\n{content}" + + def minify(self): + return f"minified {self.content}" + + +class MockBundle(object): + rx_css_import = r".*" + + def __init__(self, asset, file, env=None): + self.asset = asset + self.file = file + self.env = env + + self.javascripts = [ + MockAsset("f1", "a"), + MockAsset("f2", "b") + ] + + self.stylesheets = [ + MockAsset('c1', 'c'), + MockAsset('c2', 'd'), + MockAsset('c2', ''), + ] + + def preprocess_css(self): + result = ["prep"] + + for asset in self.stylesheets: + result.append(asset.content) + if asset.content != '': + asset.content = 'm' + + return "\n".join(result) + + +def test_assets_bundler(modules): + asset = 'base.common' + env = MagicMock() + + bundler = AssetsBundler(env, asset) + assert bundler.env == env + assert bundler.asset == asset + assert bundler._bundle is None + + bundle_path = "odoo.addons.base.models.assetsbundle.AssetsBundle" + + with patch.dict('sys.modules', modules), \ + patch(bundle_path, MockBundle): + assert isinstance(bundler.bundle, MockBundle) + assert bundler._bundle is not None + + js = bundler.get_js() + assert js == '/* f1 */\na\n/* f2 */\nb' + + js_minified = bundler.get_js(minified=True) + assert js_minified == 'minified a;\nminified b;' + + # output with headers but output not relevant, it's + # just mocked to confirm different outputs + bundler = AssetsBundler(env, asset) + css = bundler.get_css() + assert css == ( + 'prep\n\n' + 'c\n\nd\n\n\n' + '/* /* c1 */ *//* */\n' + '/* m *//* */\n' + '/* /* c2 */ *//* */\n' + '/* m *//* */' + ) + + # raw output from preprocess and matches + bundler = AssetsBundler(env, asset) + css_minified = bundler.get_css(minified=True) + assert css_minified == 'prep\n\nc\n\nd\n\n\n\n\n\n'