[FIX] base_import_module: Keep manifest assets of dynamic modules
The `ir_import_module` module is used by users to upload custom module data via a zip file. The zip file can contain xml data, xml views and static files. Before this commit, the "import module" feature of this module was broken since the imported module's manifest was lost after the import was performed. Now, 'ir.asset' records are created along with the attachments pointing to the imported static files. closes odoo/odoo#80120 X-original-commit: ec77577796e72707fbb2077f05067428cebc3c7f Signed-off-by: Julien Castiaux <juc@odoo.com> Signed-off-by: Julien Mougenot (jum) <jum@odoo.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import test_import_module
|
||||
@@ -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)
|
||||
@@ -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',
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
@@ -1 +0,0 @@
|
||||
console.log('test_module javascript');
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- TODO: make test suite -->
|
||||
<record id="base.main_company" model="res.company">
|
||||
<field name="name">The base company is noupdate=1</field>
|
||||
</record>
|
||||
|
||||
<record id="main_company2" model="res.company">
|
||||
<field name="name">Hagrid</field>
|
||||
<field name="report_header">My Company Tagline</field>
|
||||
<field name="currency_id" ref="base.EUR"/>
|
||||
</record>
|
||||
|
||||
<template id="test_page" name="Test Page">
|
||||
<t t-call="website.layout">
|
||||
<h1>
|
||||
This page comes from an imported module!
|
||||
</h1>
|
||||
<p>
|
||||
And this static image too !
|
||||
<img src="/test_module/static/src/img/c64.png" alt='Logo'/>
|
||||
</p>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="website.homepage">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
This homepage has been overwritten by an imported module !
|
||||
<p>
|
||||
<a href="/page/test_module.test_page">Link to page added by imported module</a>
|
||||
</p>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="contactus_test" name="Contact Form" inherit_id="website.contactus">
|
||||
<xpath expr="//h1" position="replace">
|
||||
<h1>This contact us title has been changed by an imported module</h1>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user