diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py index f245ead654c..73c384451ae 100644 --- a/addons/base_import_module/models/ir_module.py +++ b/addons/base_import_module/models/ir_module.py @@ -106,6 +106,43 @@ class IrModule(models.Model): else: IrAttachment.create(values) + IrAsset = self.env['ir.asset'] + assets_vals = [] + + # Generate 'ir.asset' record values for each asset delared in the manifest + for bundle, commands in terp.get('assets', {}).items(): + for command in commands: + directive, target, path = IrAsset._process_command(command) + path = path if path.startswith('/') else '/' + path # Ensures a '/' at the start + assets_vals.append({ + 'name': f'{module}.{bundle}.{path}', + 'directive': directive, + 'target': target, + 'path': path, + 'bundle': bundle, + }) + + # Look for existing assets + existing_assets = IrAsset.search([('name', 'in', [vals['name'] for vals in assets_vals])]) + existing_assets = existing_assets.mapped(lambda r: (r.name, r)) + assets_to_create = [] + + # Update existing assets and generate the list of new assets values + for values in assets_vals: + if values['name'] in existing_assets: + existing_assets[values['name']].write(values) + else: + assets_to_create.append(values) + + # Create new assets and attach 'ir.model.data' records to them + created_assets = IrAsset.create(assets_to_create) + self.env['ir.model.data'].create([{ + 'name': f"{asset['bundle']}.{asset['path']}", + 'model': 'ir.asset', + 'module': module, + 'res_id': asset.id, + } for asset in created_assets]) + return True @api.model @@ -157,8 +194,15 @@ class IrModule(models.Model): modules_to_delete = self.filtered('imported') res = super().module_uninstall() if modules_to_delete: + deleted_modules_names = modules_to_delete.mapped('name') + assets_data = self.env['ir.model.data'].search([ + ('model', '=', 'ir.asset'), + ('module', 'in', deleted_modules_names), + ]) + assets = self.env['ir.asset'].search([('id', 'in', assets_data.mapped('res_id'))]) + assets.unlink() _logger.info("deleting imported modules upon uninstallation: %s", - ", ".join(modules_to_delete.mapped('name'))) + ", ".join(deleted_modules_names)) modules_to_delete.unlink() return res diff --git a/addons/base_import_module/tests/__init__.py b/addons/base_import_module/tests/__init__.py new file mode 100644 index 00000000000..50f27409655 --- /dev/null +++ b/addons/base_import_module/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import test_import_module diff --git a/addons/base_import_module/tests/test_import_module.py b/addons/base_import_module/tests/test_import_module.py new file mode 100644 index 00000000000..2b9398999d5 --- /dev/null +++ b/addons/base_import_module/tests/test_import_module.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import json +from io import BytesIO +from zipfile import ZipFile + +import odoo.tests + +@odoo.tests.tagged('post_install', '-at_install') +class TestImportModule(odoo.tests.TransactionCase): + + def test_import_and_uninstall_module(self): + bundle = 'web.assets_backend' + path = '/test_module/static/src/js/test.js' + manifest_content = json.dumps({ + 'name': 'Test Module', + 'description': 'Test', + 'assets': { + 'web.assets_backend': [ + 'test_module/static/src/js/test.js' + ] + }, + 'license': 'LGPL-3', + }) + + stream = BytesIO() + with ZipFile(stream, 'w') as archive: + archive.writestr('test_module/__manifest__.py', manifest_content) + archive.writestr('test_module/static/src/js/test.js', "console.log('AAA');") + + # Import test module + self.env['ir.module.module'].import_zipfile(stream) + + attachment = self.env['ir.attachment'].search([('url', '=', path)]) + self.assertEqual(attachment.name, 'test.js') + self.assertEqual(attachment.type, 'binary') + self.assertEqual(attachment.raw, b"console.log('AAA');") + + asset = self.env['ir.asset'].search([('name', '=', f'test_module.{bundle}.{path}')]) + self.assertEqual(asset.path, path) + self.assertEqual(asset.bundle, bundle) + self.assertEqual(asset.directive, 'append') + self.assertEqual(asset.target, False) + + asset_data = self.env['ir.model.data'].search([('model', '=', 'ir.asset'), ('res_id', '=', asset.id)]) + self.assertEqual(asset_data.module, 'test_module') + self.assertEqual(asset_data.name, f'{bundle}.{path}') + + # Uninstall test module + self.env['ir.module.module'].search([('name', '=', 'test_module')]).module_uninstall() + + attachment = self.env['ir.attachment'].search([('url', '=', path)]) + self.assertEqual(len(attachment), 1) + + asset = self.env['ir.asset'].search([('name', '=', f'test_module.{bundle}.{path}')]) + self.assertEqual(len(asset), 0) + + asset_data = self.env['ir.model.data'].search([('model', '=', 'ir.asset'), ('res_id', '=', asset.id)]) + self.assertEqual(len(asset_data), 0) diff --git a/addons/base_import_module/tests/test_module/__manifest__.py b/addons/base_import_module/tests/test_module/__manifest__.py deleted file mode 100644 index 44cf4586abd..00000000000 --- a/addons/base_import_module/tests/test_module/__manifest__.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -{ - 'name': 'Test Module', - 'category': 'Website/Website', - 'summary': 'Custom', - 'version': '1.0', - 'description': """ - Test - """, - 'depends': ['website'], - 'data': [ - 'test.xml', - ], - 'assets': { - 'web.assets_backend': [ - 'test_module/static/src/js/test.js' - ] - }, - 'installable': True, - 'application': True, - 'license': 'LGPL-3', -} diff --git a/addons/base_import_module/tests/test_module/static/src/img/c64.png b/addons/base_import_module/tests/test_module/static/src/img/c64.png deleted file mode 100644 index 3e8183f934f..00000000000 Binary files a/addons/base_import_module/tests/test_module/static/src/img/c64.png and /dev/null differ diff --git a/addons/base_import_module/tests/test_module/static/src/js/test.js b/addons/base_import_module/tests/test_module/static/src/js/test.js deleted file mode 100644 index de803c83c34..00000000000 --- a/addons/base_import_module/tests/test_module/static/src/js/test.js +++ /dev/null @@ -1 +0,0 @@ -console.log('test_module javascript'); diff --git a/addons/base_import_module/tests/test_module/test.xml b/addons/base_import_module/tests/test_module/test.xml deleted file mode 100644 index 7ee89970262..00000000000 --- a/addons/base_import_module/tests/test_module/test.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - The base company is noupdate=1 - - - - Hagrid - My Company Tagline - - - - - - - - - - diff --git a/odoo/addons/base/models/ir_asset.py b/odoo/addons/base/models/ir_asset.py index 72289224942..ab74e233ab4 100644 --- a/odoo/addons/base/models/ir_asset.py +++ b/odoo/addons/base/models/ir_asset.py @@ -10,7 +10,6 @@ from werkzeug import urls import odoo from odoo.tools import misc from odoo import tools -from odoo.addons import __path__ as ADDONS_PATH from odoo import api, fields, http, models from odoo.http import root @@ -215,14 +214,7 @@ class IrAsset(models.Model): continue manifest_assets = manifest.get('assets', {}) for command in manifest_assets.get(bundle, []): - if isinstance(command, str): - # Default directive: append - directive, target, path_def = APPEND_DIRECTIVE, None, command - elif command[0] in DIRECTIVES_WITH_TARGET: - directive, target, path_def = command - else: - directive, path_def = command - target = None + directive, target, path_def = self._process_command(command) process_path(directive, target, path_def) # 3. Process the rest of 'ir.asset' records @@ -388,6 +380,19 @@ class IrAsset(models.Model): if not extensions or path.split('.')[-1] in extensions ] + def _process_command(self, command): + """Parses a given command to return its directive, target and path definition.""" + if isinstance(command, str): + # Default directive: append + directive, target, path_def = APPEND_DIRECTIVE, None, command + elif command[0] in DIRECTIVES_WITH_TARGET: + directive, target, path_def = command + else: + directive, path_def = command + target = None + return directive, target, path_def + + class AssetPaths: """ A list of asset paths (path, addon, bundle) with efficient operations. """ def __init__(self):