diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/base_import_module/models/ir_module.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/base_import_module/models/ir_module.py')
| -rw-r--r-- | addons/base_import_module/models/ir_module.py | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py new file mode 100644 index 00000000..f245ead6 --- /dev/null +++ b/addons/base_import_module/models/ir_module.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +import ast +import base64 +import logging +import lxml +import os +import sys +import tempfile +import zipfile +from os.path import join as opj + +import odoo +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +from odoo.modules import load_information_from_description_file +from odoo.tools import convert_file, exception_to_unicode + +_logger = logging.getLogger(__name__) + +MAX_FILE_SIZE = 100 * 1024 * 1024 # in megabytes + + +class IrModule(models.Model): + _inherit = "ir.module.module" + + imported = fields.Boolean(string="Imported Module") + + @api.depends('name') + def _get_latest_version(self): + imported_modules = self.filtered(lambda m: m.imported and m.latest_version) + for module in imported_modules: + module.installed_version = module.latest_version + super(IrModule, self - imported_modules)._get_latest_version() + + def _import_module(self, module, path, force=False): + known_mods = self.search([]) + known_mods_names = {m.name: m for m in known_mods} + installed_mods = [m.name for m in known_mods if m.state == 'installed'] + + terp = load_information_from_description_file(module, mod_path=path) + if not terp: + return False + values = self.get_values_from_terp(terp) + if 'version' in terp: + values['latest_version'] = terp['version'] + + unmet_dependencies = set(terp['depends']).difference(installed_mods) + + if unmet_dependencies: + if (unmet_dependencies == set(['web_studio']) and + _is_studio_custom(path)): + err = _("Studio customizations require Studio") + else: + err = _("Unmet module dependencies: \n\n - %s") % '\n - '.join( + known_mods.filtered(lambda mod: mod.name in unmet_dependencies).mapped('shortdesc') + ) + raise UserError(err) + elif 'web_studio' not in installed_mods and _is_studio_custom(path): + raise UserError(_("Studio customizations require the Odoo Studio app.")) + + mod = known_mods_names.get(module) + if mod: + mod.write(dict(state='installed', **values)) + mode = 'update' if not force else 'init' + else: + assert terp.get('installable', True), "Module not installable" + self.create(dict(name=module, state='installed', imported=True, **values)) + mode = 'init' + + for kind in ['data', 'init_xml', 'update_xml']: + for filename in terp[kind]: + ext = os.path.splitext(filename)[1].lower() + if ext not in ('.xml', '.csv', '.sql'): + _logger.info("module %s: skip unsupported file %s", module, filename) + continue + _logger.info("module %s: loading %s", module, filename) + noupdate = False + if ext == '.csv' and kind in ('init', 'init_xml'): + noupdate = True + pathname = opj(path, filename) + idref = {} + convert_file(self.env.cr, module, filename, idref, mode=mode, noupdate=noupdate, kind=kind, pathname=pathname) + + path_static = opj(path, 'static') + IrAttachment = self.env['ir.attachment'] + if os.path.isdir(path_static): + for root, dirs, files in os.walk(path_static): + for static_file in files: + full_path = opj(root, static_file) + with open(full_path, 'rb') as fp: + data = base64.b64encode(fp.read()) + url_path = '/{}{}'.format(module, full_path.split(path)[1].replace(os.path.sep, '/')) + if not isinstance(url_path, str): + url_path = url_path.decode(sys.getfilesystemencoding()) + filename = os.path.split(url_path)[1] + values = dict( + name=filename, + url=url_path, + res_model='ir.ui.view', + type='binary', + datas=data, + ) + attachment = IrAttachment.search([('url', '=', url_path), ('type', '=', 'binary'), ('res_model', '=', 'ir.ui.view')]) + if attachment: + attachment.write(values) + else: + IrAttachment.create(values) + + return True + + @api.model + def import_zipfile(self, module_file, force=False): + if not module_file: + raise Exception(_("No file sent.")) + if not zipfile.is_zipfile(module_file): + raise UserError(_('Only zip files are supported.')) + + success = [] + errors = dict() + module_names = [] + with zipfile.ZipFile(module_file, "r") as z: + for zf in z.filelist: + if zf.file_size > MAX_FILE_SIZE: + raise UserError(_("File '%s' exceed maximum allowed file size", zf.filename)) + + with tempfile.TemporaryDirectory() as module_dir: + import odoo.modules.module as module + try: + odoo.addons.__path__.append(module_dir) + z.extractall(module_dir) + dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))] + for mod_name in dirs: + module_names.append(mod_name) + try: + # assert mod_name.startswith('theme_') + path = opj(module_dir, mod_name) + if self._import_module(mod_name, path, force=force): + success.append(mod_name) + except Exception as e: + _logger.exception('Error while importing module') + errors[mod_name] = exception_to_unicode(e) + finally: + odoo.addons.__path__.remove(module_dir) + r = ["Successfully imported module '%s'" % mod for mod in success] + for mod, error in errors.items(): + r.append("Error while importing module '%s'.\n\n %s \n Make sure those modules are installed and try again." % (mod, error)) + return '\n'.join(r), module_names + + def module_uninstall(self): + # Delete an ir_module_module record completely if it was an imported + # one. The rationale behind this is that an imported module *cannot* be + # reinstalled anyway, as it requires the data files. Any attempt to + # install it again will simply fail without trace. + # /!\ modules_to_delete must be calculated before calling super().module_uninstall(), + # because when uninstalling `base_import_module` the `imported` column will no longer be + # in the database but we'll still have an old registry that runs this code. + modules_to_delete = self.filtered('imported') + res = super().module_uninstall() + if modules_to_delete: + _logger.info("deleting imported modules upon uninstallation: %s", + ", ".join(modules_to_delete.mapped('name'))) + modules_to_delete.unlink() + return res + + +def _is_studio_custom(path): + """ + Checks the to-be-imported records to see if there are any references to + studio, which would mean that the module was created using studio + + Returns True if any of the records contains a context with the key + studio in it, False if none of the records do + """ + filepaths = [] + for level in os.walk(path): + filepaths += [os.path.join(level[0], fn) for fn in level[2]] + filepaths = [fp for fp in filepaths if fp.lower().endswith('.xml')] + + for fp in filepaths: + root = lxml.etree.parse(fp).getroot() + + for record in root: + # there might not be a context if it's a non-studio module + try: + # ast.literal_eval is like eval(), but safer + # context is a string representing a python dict + ctx = ast.literal_eval(record.get('context')) + # there are no cases in which studio is false + # so just checking for its existence is enough + if ctx and ctx.get('studio'): + return True + except Exception: + continue + return False |
