[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:
Julien Mougenot
2021-11-22 11:12:44 +00:00
parent 34aa881992
commit 54b45aec9e
8 changed files with 121 additions and 78 deletions
+45 -1
View File
@@ -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>
+14 -9
View File
@@ -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):