Browse Source
Merge pull request #1217 from acsone/11.0-forward_prod_from_10.0-bwi
Merge pull request #1217 from acsone/11.0-forward_prod_from_10.0-bwi
[11.0] module_auto_update: forward port from 10.0pull/1221/merge
Pedro M. Baeza
7 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 868 additions and 387 deletions
-
64module_auto_update/README.rst
-
2module_auto_update/__init__.py
-
15module_auto_update/__manifest__.py
-
45module_auto_update/addon_hash.py
-
2module_auto_update/data/cron_data_deprecated.xml
-
16module_auto_update/hooks.py
-
25module_auto_update/migrations/10.0.2.0.0/pre-migrate.py
-
1module_auto_update/models/__init__.py
-
179module_auto_update/models/module.py
-
68module_auto_update/models/module_deprecated.py
-
4module_auto_update/tests/__init__.py
-
1module_auto_update/tests/sample_module/README.rst
-
1module_auto_update/tests/sample_module/data/f1.xml
-
1module_auto_update/tests/sample_module/data/f2.xml
-
1module_auto_update/tests/sample_module/i18n/en.po
-
1module_auto_update/tests/sample_module/i18n/en_US.po
-
1module_auto_update/tests/sample_module/i18n/fr.po
-
1module_auto_update/tests/sample_module/i18n/fr_BE.po
-
1module_auto_update/tests/sample_module/i18n/test.pot
-
1module_auto_update/tests/sample_module/i18n_extra/en.po
-
1module_auto_update/tests/sample_module/i18n_extra/fr.po
-
1module_auto_update/tests/sample_module/i18n_extra/nl_NL.po
-
1module_auto_update/tests/sample_module/models/stuff.py
-
BINmodule_auto_update/tests/sample_module/models/stuff.pyc
-
BINmodule_auto_update/tests/sample_module/models/stuff.pyo
-
1module_auto_update/tests/sample_module/static/src/some.js
-
68module_auto_update/tests/test_addon_hash.py
-
334module_auto_update/tests/test_module.py
-
210module_auto_update/tests/test_module_deprecated.py
-
3module_auto_update/tests/test_module_upgrade_deprecated.py
-
49module_auto_update/views/module_views.xml
-
2module_auto_update/wizards/__init__.py
-
49module_auto_update/wizards/module_upgrade.py
-
84module_auto_update/wizards/module_upgrade_deprecated.py
@ -0,0 +1,45 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2018 ACSONE SA/NV. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
from fnmatch import fnmatch |
||||
|
import hashlib |
||||
|
import os |
||||
|
|
||||
|
|
||||
|
def _fnmatch(filename, patterns): |
||||
|
for pattern in patterns: |
||||
|
if fnmatch(filename, pattern): |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def _walk(top, exclude_patterns, keep_langs): |
||||
|
keep_langs = {l.split('_')[0] for l in keep_langs} |
||||
|
for dirpath, dirnames, filenames in os.walk(top): |
||||
|
dirnames.sort() |
||||
|
reldir = os.path.relpath(dirpath, top) |
||||
|
if reldir == '.': |
||||
|
reldir = '' |
||||
|
for filename in sorted(filenames): |
||||
|
filepath = os.path.join(reldir, filename) |
||||
|
if _fnmatch(filepath, exclude_patterns): |
||||
|
continue |
||||
|
if keep_langs and reldir in {'i18n', 'i18n_extra'}: |
||||
|
basename, ext = os.path.splitext(filename) |
||||
|
if ext == '.po': |
||||
|
if basename.split('_')[0] not in keep_langs: |
||||
|
continue |
||||
|
yield filepath |
||||
|
|
||||
|
|
||||
|
def addon_hash(top, exclude_patterns, keep_langs): |
||||
|
"""Compute a sha1 digest of file contents.""" |
||||
|
m = hashlib.sha1() |
||||
|
for filepath in _walk(top, exclude_patterns, keep_langs): |
||||
|
# hash filename so empty files influence the hash |
||||
|
m.update(filepath.encode('utf-8')) |
||||
|
# hash file content |
||||
|
with open(os.path.join(top, filepath), 'rb') as f: |
||||
|
m.update(f.read()) |
||||
|
return m.hexdigest() |
@ -0,0 +1,25 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2018 Tecnativa - Jairo Llopis |
||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). |
||||
|
import logging |
||||
|
from psycopg2 import IntegrityError |
||||
|
from odoo.addons.module_auto_update.models.module_deprecated import \ |
||||
|
PARAM_DEPRECATED |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
def migrate(cr, version): |
||||
|
"""Autoenable deprecated behavior.""" |
||||
|
try: |
||||
|
with cr.savepoint(): |
||||
|
cr.execute( |
||||
|
"""INSERT INTO ir_config_parameter (key, value) |
||||
|
VALUES (%s, '1')""", |
||||
|
(PARAM_DEPRECATED,) |
||||
|
) |
||||
|
_logger.warn("Deprecated features have been autoenabled, see " |
||||
|
"addon's README to know how to upgrade to the new " |
||||
|
"supported autoupdate mechanism.") |
||||
|
except IntegrityError: |
||||
|
_logger.info("Deprecated features setting exists, not autoenabling") |
@ -1,3 +1,4 @@ |
|||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
from . import module |
from . import module |
||||
|
from . import module_deprecated |
@ -1,69 +1,148 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
# Copyright 2017 LasLabs Inc. |
# Copyright 2017 LasLabs Inc. |
||||
|
# Copyright 2018 ACSONE SA/NV. |
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
import json |
||||
import logging |
import logging |
||||
|
import os |
||||
|
|
||||
from odoo import api, fields, models |
|
||||
|
from odoo import api, exceptions, models, tools |
||||
from odoo.modules.module import get_module_path |
from odoo.modules.module import get_module_path |
||||
|
|
||||
|
from ..addon_hash import addon_hash |
||||
|
|
||||
|
PARAM_INSTALLED_CHECKSUMS = \ |
||||
|
'module_auto_update.installed_checksums' |
||||
|
PARAM_EXCLUDE_PATTERNS = \ |
||||
|
'module_auto_update.exclude_patterns' |
||||
|
DEFAULT_EXCLUDE_PATTERNS = \ |
||||
|
'*.pyc,*.pyo,i18n/*.pot,i18n_extra/*.pot,static/*' |
||||
|
|
||||
_logger = logging.getLogger(__name__) |
_logger = logging.getLogger(__name__) |
||||
try: |
|
||||
from checksumdir import dirhash |
|
||||
except ImportError: |
|
||||
_logger.debug('Cannot `import checksumdir`.') |
|
||||
|
|
||||
|
|
||||
|
class IncompleteUpgradeError(exceptions.UserError): |
||||
|
pass |
||||
|
|
||||
|
|
||||
class Module(models.Model): |
class Module(models.Model): |
||||
_inherit = 'ir.module.module' |
_inherit = 'ir.module.module' |
||||
|
|
||||
checksum_dir = fields.Char( |
|
||||
compute='_compute_checksum_dir', |
|
||||
) |
|
||||
checksum_installed = fields.Char() |
|
||||
|
|
||||
@api.depends('name') |
|
||||
def _compute_checksum_dir(self): |
|
||||
exclude = self.env["ir.config_parameter"].get_param( |
|
||||
"module_auto_update.checksum_excluded_extensions", |
|
||||
"pyc,pyo", |
|
||||
).split(",") |
|
||||
|
|
||||
for r in self: |
|
||||
try: |
|
||||
r.checksum_dir = dirhash( |
|
||||
get_module_path(r.name), |
|
||||
'sha1', |
|
||||
excluded_extensions=exclude, |
|
||||
|
@api.multi |
||||
|
def _get_checksum_dir(self): |
||||
|
self.ensure_one() |
||||
|
|
||||
|
exclude_patterns = self.env["ir.config_parameter"].get_param( |
||||
|
PARAM_EXCLUDE_PATTERNS, |
||||
|
DEFAULT_EXCLUDE_PATTERNS, |
||||
) |
) |
||||
except TypeError: |
|
||||
_logger.debug( |
|
||||
"Cannot compute dir hash for %s, module not found", |
|
||||
r.display_name) |
|
||||
|
exclude_patterns = [p.strip() for p in exclude_patterns.split(',')] |
||||
|
keep_langs = self.env['res.lang'].search([]).mapped('code') |
||||
|
|
||||
@api.multi |
|
||||
def _store_checksum_installed(self, vals): |
|
||||
"""Store the right installed checksum, if addon is installed.""" |
|
||||
if 'checksum_installed' not in vals: |
|
||||
try: |
|
||||
version = vals["latest_version"] |
|
||||
except KeyError: |
|
||||
return # Not [un]installing/updating any addon |
|
||||
if version is False: |
|
||||
# Uninstalling |
|
||||
self.write({'checksum_installed': False}) |
|
||||
|
module_path = get_module_path(self.name) |
||||
|
if module_path and os.path.isdir(module_path): |
||||
|
checksum_dir = addon_hash( |
||||
|
module_path, |
||||
|
exclude_patterns, |
||||
|
keep_langs, |
||||
|
) |
||||
else: |
else: |
||||
# Installing or updating |
|
||||
for one in self: |
|
||||
one.checksum_installed = one.checksum_dir |
|
||||
|
checksum_dir = False |
||||
|
|
||||
|
return checksum_dir |
||||
|
|
||||
@api.model |
@api.model |
||||
def create(self, vals): |
|
||||
res = super(Module, self).create(vals) |
|
||||
res._store_checksum_installed(vals) |
|
||||
return res |
|
||||
|
def _get_saved_checksums(self): |
||||
|
Icp = self.env['ir.config_parameter'] |
||||
|
return json.loads(Icp.get_param(PARAM_INSTALLED_CHECKSUMS, '{}')) |
||||
|
|
||||
@api.multi |
|
||||
def write(self, vals): |
|
||||
res = super(Module, self).write(vals) |
|
||||
self._store_checksum_installed(vals) |
|
||||
return res |
|
||||
|
@api.model |
||||
|
def _save_checksums(self, checksums): |
||||
|
Icp = self.env['ir.config_parameter'] |
||||
|
Icp.set_param(PARAM_INSTALLED_CHECKSUMS, json.dumps(checksums)) |
||||
|
|
||||
|
@api.model |
||||
|
def _save_installed_checksums(self): |
||||
|
checksums = {} |
||||
|
installed_modules = self.search([('state', '=', 'installed')]) |
||||
|
for module in installed_modules: |
||||
|
checksums[module.name] = module._get_checksum_dir() |
||||
|
self._save_checksums(checksums) |
||||
|
|
||||
|
@api.model |
||||
|
def _get_modules_partially_installed(self): |
||||
|
return self.search([ |
||||
|
('state', 'in', ('to install', 'to remove', 'to upgrade')), |
||||
|
]) |
||||
|
|
||||
|
@api.model |
||||
|
def _get_modules_with_changed_checksum(self): |
||||
|
saved_checksums = self._get_saved_checksums() |
||||
|
installed_modules = self.search([('state', '=', 'installed')]) |
||||
|
return installed_modules.filtered( |
||||
|
lambda r: r._get_checksum_dir() != saved_checksums.get(r.name), |
||||
|
) |
||||
|
|
||||
|
@api.model |
||||
|
def upgrade_changed_checksum(self, overwrite_existing_translations=False): |
||||
|
"""Run an upgrade of the database, upgrading only changed modules. |
||||
|
|
||||
|
Installed modules for which the checksum has changed since the |
||||
|
last successful run of this method are marked "to upgrade", |
||||
|
then the normal Odoo scheduled upgrade process |
||||
|
is launched. |
||||
|
|
||||
|
If there is no module with a changed checksum, and no module in state |
||||
|
other than installed, uninstalled, uninstallable, this method does |
||||
|
nothing, otherwise the normal Odoo upgrade process is launched. |
||||
|
|
||||
|
After a successful upgrade, the checksums of installed modules are |
||||
|
saved. |
||||
|
|
||||
|
In case of error during the upgrade, an exception is raised. |
||||
|
If any module remains to upgrade or to uninstall after the upgrade |
||||
|
process, an exception is raised as well. |
||||
|
|
||||
|
Note: this method commits the current transaction at each important |
||||
|
step, it is therefore not intended to be run as part of a |
||||
|
larger transaction. |
||||
|
""" |
||||
|
_logger.info( |
||||
|
"Checksum upgrade starting (i18n-overwrite=%s)...", |
||||
|
overwrite_existing_translations |
||||
|
) |
||||
|
|
||||
|
tools.config['overwrite_existing_translations'] = \ |
||||
|
overwrite_existing_translations |
||||
|
|
||||
|
_logger.info("Updating modules list...") |
||||
|
self.update_list() |
||||
|
changed_modules = self._get_modules_with_changed_checksum() |
||||
|
if not changed_modules and not self._get_modules_partially_installed(): |
||||
|
_logger.info("No checksum change detected in installed modules " |
||||
|
"and all modules installed, nothing to do.") |
||||
|
return |
||||
|
_logger.info("Marking the following modules to upgrade, " |
||||
|
"for their checksums changed: %s...", |
||||
|
','.join(changed_modules.mapped('name'))) |
||||
|
changed_modules.button_upgrade() |
||||
|
self.env.cr.commit() # pylint: disable=invalid-commit |
||||
|
|
||||
|
_logger.info("Upgrading...") |
||||
|
self.env['base.module.upgrade'].upgrade_module() |
||||
|
self.env.cr.commit() # pylint: disable=invalid-commit |
||||
|
|
||||
|
_logger.info("Upgrade successful, updating checksums...") |
||||
|
self._save_installed_checksums() |
||||
|
self.env.cr.commit() # pylint: disable=invalid-commit |
||||
|
|
||||
|
partial_modules = self._get_modules_partially_installed() |
||||
|
if partial_modules: |
||||
|
raise IncompleteUpgradeError( |
||||
|
"Checksum upgrade successful " |
||||
|
"but incomplete for the following modules: %s" % |
||||
|
','.join(partial_modules.mapped('name')) |
||||
|
) |
||||
|
|
||||
|
_logger.info("Checksum upgrade complete.") |
@ -0,0 +1,68 @@ |
|||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
from odoo import api, fields, models |
||||
|
|
||||
|
PARAM_DEPRECATED = "module_auto_update.enable_deprecated" |
||||
|
|
||||
|
|
||||
|
class Module(models.Model): |
||||
|
_inherit = 'ir.module.module' |
||||
|
|
||||
|
checksum_dir = fields.Char( |
||||
|
deprecated=True, |
||||
|
compute='_compute_checksum_dir', |
||||
|
) |
||||
|
checksum_installed = fields.Char( |
||||
|
deprecated=True, |
||||
|
compute='_compute_checksum_installed', |
||||
|
inverse='_inverse_checksum_installed', |
||||
|
store=False, |
||||
|
) |
||||
|
|
||||
|
@api.depends('name') |
||||
|
def _compute_checksum_dir(self): |
||||
|
for rec in self: |
||||
|
rec.checksum_dir = rec._get_checksum_dir() |
||||
|
|
||||
|
def _compute_checksum_installed(self): |
||||
|
saved_checksums = self._get_saved_checksums() |
||||
|
for rec in self: |
||||
|
rec.checksum_installed = saved_checksums.get(rec.name, False) |
||||
|
|
||||
|
def _inverse_checksum_installed(self): |
||||
|
checksums = self._get_saved_checksums() |
||||
|
for rec in self: |
||||
|
checksums[rec.name] = rec.checksum_installed |
||||
|
self._save_checksums(checksums) |
||||
|
|
||||
|
@api.multi |
||||
|
def _store_checksum_installed(self, vals): |
||||
|
"""Store the right installed checksum, if addon is installed.""" |
||||
|
if not self.env["base.module.upgrade"]._autoupdate_deprecated(): |
||||
|
# Skip if deprecated features are disabled |
||||
|
return |
||||
|
if 'checksum_installed' not in vals: |
||||
|
try: |
||||
|
version = vals["latest_version"] |
||||
|
except KeyError: |
||||
|
return # Not [un]installing/updating any addon |
||||
|
if version is False: |
||||
|
# Uninstalling |
||||
|
self.write({'checksum_installed': False}) |
||||
|
else: |
||||
|
# Installing or updating |
||||
|
for one in self: |
||||
|
one.checksum_installed = one.checksum_dir |
||||
|
|
||||
|
@api.model |
||||
|
def create(self, vals): |
||||
|
res = super(Module, self).create(vals) |
||||
|
res._store_checksum_installed(vals) |
||||
|
return res |
||||
|
|
||||
|
@api.multi |
||||
|
def write(self, vals): |
||||
|
res = super(Module, self).write(vals) |
||||
|
self._store_checksum_installed(vals) |
||||
|
return res |
@ -1,4 +1,6 @@ |
|||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
from . import test_addon_hash |
||||
from . import test_module |
from . import test_module |
||||
from . import test_module_upgrade |
|
||||
|
from . import test_module_deprecated |
||||
|
from . import test_module_upgrade_deprecated |
@ -0,0 +1 @@ |
|||||
|
Test data for addon_hash module. |
@ -0,0 +1 @@ |
|||||
|
<odoo/> |
@ -0,0 +1 @@ |
|||||
|
<odoo/> |
@ -0,0 +1 @@ |
|||||
|
en text |
@ -0,0 +1 @@ |
|||||
|
en_US |
@ -0,0 +1 @@ |
|||||
|
fr |
@ -0,0 +1 @@ |
|||||
|
fr_BE |
@ -0,0 +1 @@ |
|||||
|
... |
@ -0,0 +1 @@ |
|||||
|
en |
@ -0,0 +1 @@ |
|||||
|
fr |
@ -0,0 +1 @@ |
|||||
|
nl_NL |
@ -0,0 +1 @@ |
|||||
|
1+1 |
@ -0,0 +1 @@ |
|||||
|
/* javascript */ |
@ -0,0 +1,68 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2018 ACSONE SA/NV. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
import os |
||||
|
import unittest |
||||
|
|
||||
|
from .. import addon_hash |
||||
|
from ..models.module import DEFAULT_EXCLUDE_PATTERNS |
||||
|
|
||||
|
|
||||
|
class TestAddonHash(unittest.TestCase): |
||||
|
|
||||
|
def setUp(self): |
||||
|
super(TestAddonHash, self).setUp() |
||||
|
self.sample_dir = os.path.join( |
||||
|
os.path.dirname(__file__), |
||||
|
'sample_module', |
||||
|
) |
||||
|
|
||||
|
def test_basic(self): |
||||
|
files = list(addon_hash._walk( |
||||
|
self.sample_dir, |
||||
|
exclude_patterns=[], |
||||
|
keep_langs=[], |
||||
|
)) |
||||
|
self.assertEqual(files, [ |
||||
|
'README.rst', |
||||
|
'data/f1.xml', |
||||
|
'data/f2.xml', |
||||
|
'i18n/en.po', |
||||
|
'i18n/en_US.po', |
||||
|
'i18n/fr.po', |
||||
|
'i18n/fr_BE.po', |
||||
|
'i18n/test.pot', |
||||
|
'i18n_extra/en.po', |
||||
|
'i18n_extra/fr.po', |
||||
|
'i18n_extra/nl_NL.po', |
||||
|
'models/stuff.py', |
||||
|
'models/stuff.pyc', |
||||
|
'models/stuff.pyo', |
||||
|
'static/src/some.js', |
||||
|
]) |
||||
|
|
||||
|
def test_exclude(self): |
||||
|
files = list(addon_hash._walk( |
||||
|
self.sample_dir, |
||||
|
exclude_patterns=DEFAULT_EXCLUDE_PATTERNS.split(','), |
||||
|
keep_langs=['fr_FR', 'nl'], |
||||
|
)) |
||||
|
self.assertEqual(files, [ |
||||
|
'README.rst', |
||||
|
'data/f1.xml', |
||||
|
'data/f2.xml', |
||||
|
'i18n/fr.po', |
||||
|
'i18n/fr_BE.po', |
||||
|
'i18n_extra/fr.po', |
||||
|
'i18n_extra/nl_NL.po', |
||||
|
'models/stuff.py', |
||||
|
]) |
||||
|
|
||||
|
def test2(self): |
||||
|
checksum = addon_hash.addon_hash( |
||||
|
self.sample_dir, |
||||
|
exclude_patterns=['*.pyc', '*.pyo', '*.pot', 'static/*'], |
||||
|
keep_langs=['fr_FR', 'nl'], |
||||
|
) |
||||
|
self.assertEqual(checksum, 'fecb89486c8a29d1f760cbd01c1950f6e8421b14') |
@ -0,0 +1,210 @@ |
|||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
import os |
||||
|
|
||||
|
import mock |
||||
|
|
||||
|
from odoo.modules import get_module_path |
||||
|
from odoo.tests.common import TransactionCase |
||||
|
from odoo.tools import mute_logger |
||||
|
|
||||
|
from odoo.addons.module_auto_update.addon_hash import addon_hash |
||||
|
|
||||
|
from ..models.module_deprecated import PARAM_DEPRECATED |
||||
|
|
||||
|
|
||||
|
model = 'odoo.addons.module_auto_update.models.module' |
||||
|
|
||||
|
|
||||
|
class EndTestException(Exception): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
class TestModule(TransactionCase): |
||||
|
|
||||
|
def setUp(self): |
||||
|
super(TestModule, self).setUp() |
||||
|
module_name = 'module_auto_update' |
||||
|
self.env["ir.config_parameter"].set_param(PARAM_DEPRECATED, "1") |
||||
|
self.own_module = self.env['ir.module.module'].search([ |
||||
|
('name', '=', module_name), |
||||
|
]) |
||||
|
self.own_dir_path = get_module_path(module_name) |
||||
|
keep_langs = self.env['res.lang'].search([]).mapped('code') |
||||
|
self.own_checksum = addon_hash( |
||||
|
self.own_dir_path, |
||||
|
exclude_patterns=['*.pyc', '*.pyo', '*.pot', 'static/*'], |
||||
|
keep_langs=keep_langs, |
||||
|
) |
||||
|
self.own_writeable = os.access(self.own_dir_path, os.W_OK) |
||||
|
|
||||
|
@mock.patch('%s.get_module_path' % model) |
||||
|
def create_test_module(self, vals, get_module_path_mock): |
||||
|
get_module_path_mock.return_value = self.own_dir_path |
||||
|
test_module = self.env['ir.module.module'].create(vals) |
||||
|
return test_module |
||||
|
|
||||
|
def test_store_checksum_installed_state_installed(self): |
||||
|
"""It should set the module's checksum_installed equal to |
||||
|
checksum_dir when vals contain a ``latest_version`` str.""" |
||||
|
self.own_module.checksum_installed = 'test' |
||||
|
self.own_module._store_checksum_installed({'latest_version': '1.0'}) |
||||
|
self.assertEqual( |
||||
|
self.own_module.checksum_installed, self.own_module.checksum_dir, |
||||
|
) |
||||
|
|
||||
|
def test_store_checksum_installed_state_uninstalled(self): |
||||
|
"""It should clear the module's checksum_installed when vals |
||||
|
contain ``"latest_version": False``""" |
||||
|
self.own_module.checksum_installed = 'test' |
||||
|
self.own_module._store_checksum_installed({'latest_version': False}) |
||||
|
self.assertIs(self.own_module.checksum_installed, False) |
||||
|
|
||||
|
def test_store_checksum_installed_vals_contain_checksum_installed(self): |
||||
|
"""It should not set checksum_installed to False or checksum_dir when |
||||
|
a checksum_installed is included in vals""" |
||||
|
self.own_module.checksum_installed = 'test' |
||||
|
self.own_module._store_checksum_installed({ |
||||
|
'state': 'installed', |
||||
|
'checksum_installed': 'test', |
||||
|
}) |
||||
|
self.assertEqual( |
||||
|
self.own_module.checksum_installed, 'test', |
||||
|
'Providing checksum_installed in vals did not prevent overwrite', |
||||
|
) |
||||
|
|
||||
|
def test_store_checksum_installed_with_retain_context(self): |
||||
|
"""It should not set checksum_installed to False or checksum_dir when |
||||
|
self has context retain_checksum_installed=True""" |
||||
|
self.own_module.checksum_installed = 'test' |
||||
|
self.own_module.with_context( |
||||
|
retain_checksum_installed=True, |
||||
|
)._store_checksum_installed({'state': 'installed'}) |
||||
|
self.assertEqual( |
||||
|
self.own_module.checksum_installed, 'test', |
||||
|
'Providing retain_checksum_installed context did not prevent ' |
||||
|
'overwrite', |
||||
|
) |
||||
|
|
||||
|
@mock.patch('%s.get_module_path' % model) |
||||
|
def test_button_uninstall_no_recompute(self, module_path_mock): |
||||
|
"""It should not attempt update on `button_uninstall`.""" |
||||
|
module_path_mock.return_value = self.own_dir_path |
||||
|
vals = { |
||||
|
'name': 'module_auto_update_test_module', |
||||
|
'state': 'installed', |
||||
|
} |
||||
|
test_module = self.create_test_module(vals) |
||||
|
test_module.checksum_installed = 'test' |
||||
|
uninstall_module = self.env['ir.module.module'].search([ |
||||
|
('name', '=', 'web'), |
||||
|
]) |
||||
|
uninstall_module.button_uninstall() |
||||
|
self.assertNotEqual( |
||||
|
test_module.state, 'to upgrade', |
||||
|
'Auto update logic was triggered during uninstall.', |
||||
|
) |
||||
|
|
||||
|
def test_button_immediate_uninstall_no_recompute(self): |
||||
|
"""It should not attempt update on `button_immediate_uninstall`.""" |
||||
|
|
||||
|
uninstall_module = self.env['ir.module.module'].search([ |
||||
|
('name', '=', 'web'), |
||||
|
]) |
||||
|
|
||||
|
try: |
||||
|
mk = mock.MagicMock() |
||||
|
uninstall_module._patch_method('button_uninstall', mk) |
||||
|
mk.side_effect = EndTestException |
||||
|
with self.assertRaises(EndTestException): |
||||
|
uninstall_module.button_immediate_uninstall() |
||||
|
finally: |
||||
|
uninstall_module._revert_method('button_uninstall') |
||||
|
|
||||
|
def test_button_uninstall_cancel(self): |
||||
|
"""It should preserve checksum_installed when cancelling uninstall""" |
||||
|
self.own_module.write({'state': 'to remove'}) |
||||
|
self.own_module.checksum_installed = 'test' |
||||
|
self.own_module.button_uninstall_cancel() |
||||
|
self.assertEqual( |
||||
|
self.own_module.checksum_installed, 'test', |
||||
|
'Uninstall cancellation does not preserve checksum_installed', |
||||
|
) |
||||
|
|
||||
|
def test_button_upgrade_cancel(self): |
||||
|
"""It should preserve checksum_installed when cancelling upgrades""" |
||||
|
self.own_module.write({'state': 'to upgrade'}) |
||||
|
self.own_module.checksum_installed = 'test' |
||||
|
self.own_module.button_upgrade_cancel() |
||||
|
self.assertEqual( |
||||
|
self.own_module.checksum_installed, 'test', |
||||
|
'Upgrade cancellation does not preserve checksum_installed', |
||||
|
) |
||||
|
|
||||
|
def test_create(self): |
||||
|
"""It should call _store_checksum_installed method""" |
||||
|
_store_checksum_installed_mock = mock.MagicMock() |
||||
|
try: |
||||
|
self.env['ir.module.module']._patch_method( |
||||
|
'_store_checksum_installed', |
||||
|
_store_checksum_installed_mock, |
||||
|
) |
||||
|
vals = { |
||||
|
'name': 'module_auto_update_test_module', |
||||
|
'state': 'installed', |
||||
|
} |
||||
|
self.create_test_module(vals) |
||||
|
_store_checksum_installed_mock.assert_called_once_with(vals) |
||||
|
finally: |
||||
|
self.env['ir.module.module']._revert_method( |
||||
|
'_store_checksum_installed', |
||||
|
) |
||||
|
|
||||
|
@mute_logger("openerp.modules.module") |
||||
|
@mock.patch('%s.get_module_path' % model) |
||||
|
def test_get_module_list(self, module_path_mock): |
||||
|
"""It should change the state of modules with different |
||||
|
checksum_dir and checksum_installed to 'to upgrade'""" |
||||
|
module_path_mock.return_value = self.own_dir_path |
||||
|
vals = { |
||||
|
'name': 'module_auto_update_test_module', |
||||
|
'state': 'installed', |
||||
|
} |
||||
|
test_module = self.create_test_module(vals) |
||||
|
test_module.checksum_installed = 'test' |
||||
|
self.env['base.module.upgrade'].get_module_list() |
||||
|
self.assertEqual( |
||||
|
test_module.state, 'to upgrade', |
||||
|
'List update does not mark upgradeable modules "to upgrade"', |
||||
|
) |
||||
|
|
||||
|
@mock.patch('%s.get_module_path' % model) |
||||
|
def test_get_module_list_only_changes_installed(self, module_path_mock): |
||||
|
"""It should not change the state of a module with a former state |
||||
|
other than 'installed' to 'to upgrade'""" |
||||
|
module_path_mock.return_value = self.own_dir_path |
||||
|
vals = { |
||||
|
'name': 'module_auto_update_test_module', |
||||
|
'state': 'uninstalled', |
||||
|
} |
||||
|
test_module = self.create_test_module(vals) |
||||
|
self.env['base.module.upgrade'].get_module_list() |
||||
|
self.assertNotEqual( |
||||
|
test_module.state, 'to upgrade', |
||||
|
'List update changed state of an uninstalled module', |
||||
|
) |
||||
|
|
||||
|
def test_write(self): |
||||
|
"""It should call _store_checksum_installed method""" |
||||
|
_store_checksum_installed_mock = mock.MagicMock() |
||||
|
self.env['ir.module.module']._patch_method( |
||||
|
'_store_checksum_installed', |
||||
|
_store_checksum_installed_mock, |
||||
|
) |
||||
|
vals = {'state': 'installed'} |
||||
|
self.own_module.write(vals) |
||||
|
_store_checksum_installed_mock.assert_called_once_with(vals) |
||||
|
self.env['ir.module.module']._revert_method( |
||||
|
'_store_checksum_installed', |
||||
|
) |
@ -1,49 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<odoo> |
|
||||
<!-- Module Search View --> |
|
||||
<record id="module_view_search" model="ir.ui.view"> |
|
||||
<field name="name">updates.module.search</field> |
|
||||
<field name="model">ir.module.module</field> |
|
||||
<field name="inherit_id" ref="base.view_module_filter"/> |
|
||||
<field name="arch" type="xml"> |
|
||||
<field name="category_id" position="after"> |
|
||||
<filter name="scheduled_upgrades" string="Scheduled Upgrades" domain="[('state', '=', 'to upgrade')]"/> |
|
||||
</field> |
|
||||
</field> |
|
||||
</record> |
|
||||
|
|
||||
<!--Open Updates Action (updates apps list first)--> |
|
||||
<record id="module_action_open_updates" model="ir.actions.server"> |
|
||||
<field name="name">Open Updates and Update Apps List Server Action</field> |
|
||||
<field name="model_id" ref="model_ir_module_module"/> |
|
||||
<field name="code"> |
|
||||
if model.update_list(): |
|
||||
action = { |
|
||||
'name': 'Updates', |
|
||||
'type': 'ir.actions.act_window', |
|
||||
'res_model': 'ir.module.module', |
|
||||
'view_type': 'form', |
|
||||
'view_mode': 'tree,form', |
|
||||
'target': 'main', |
|
||||
'context': '{"search_default_scheduled_upgrades": 1}', |
|
||||
} |
|
||||
</field> |
|
||||
</record> |
|
||||
|
|
||||
<!--Apps / Updates menu item--> |
|
||||
<menuitem |
|
||||
name="Updates" |
|
||||
action="module_action_open_updates" |
|
||||
id="module_menu_updates" |
|
||||
groups="base.group_no_one" |
|
||||
parent="base.menu_management" |
|
||||
sequence="20"/> |
|
||||
|
|
||||
<!-- Menu in Settings > Technical for standard Updates link --> |
|
||||
<menuitem parent="base.menu_custom" sequence="27" name="Modules" id="menu_default_modules"/> |
|
||||
|
|
||||
<!-- Moved standard Updates link --> |
|
||||
<record model="ir.ui.menu" id="base.menu_module_updates"> |
|
||||
<field name="parent_id" ref="menu_default_modules"/> |
|
||||
</record> |
|
||||
</odoo> |
|
@ -1,3 +1,3 @@ |
|||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
from . import module_upgrade |
|
||||
|
from . import module_upgrade_deprecated |
@ -1,49 +0,0 @@ |
|||||
# Copyright 2017 LasLabs Inc. |
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|
||||
|
|
||||
from odoo import api, models |
|
||||
|
|
||||
|
|
||||
class ModuleUpgrade(models.TransientModel): |
|
||||
_inherit = 'base.module.upgrade' |
|
||||
|
|
||||
@api.model |
|
||||
@api.returns('ir.module.module') |
|
||||
def get_module_list(self): |
|
||||
"""Set modules to upgrade searching by their dir checksum.""" |
|
||||
Module = self.env["ir.module.module"] |
|
||||
installed_modules = Module.search([('state', '=', 'installed')]) |
|
||||
upgradeable_modules = installed_modules.filtered( |
|
||||
lambda r: r.checksum_dir != r.checksum_installed, |
|
||||
) |
|
||||
upgradeable_modules.button_upgrade() |
|
||||
return super(ModuleUpgrade, self).get_module_list() |
|
||||
|
|
||||
@api.multi |
|
||||
def upgrade_module(self): |
|
||||
"""Make a fully automated addon upgrade.""" |
|
||||
# Compute updates by checksum when called in @api.model fashion |
|
||||
if not self: |
|
||||
self.get_module_list() |
|
||||
Module = self.env["ir.module.module"] |
|
||||
# Get every addon state before updating |
|
||||
pre_states = {addon["name"]: addon["state"] |
|
||||
for addon in Module.search_read([], ["name", "state"])} |
|
||||
# Perform upgrades, possibly in a limited graph that excludes me |
|
||||
result = super(ModuleUpgrade, self).upgrade_module() |
|
||||
# Reload environments, anything may have changed |
|
||||
self.env.clear() |
|
||||
# Update addons checksum if state changed and I wasn't uninstalled |
|
||||
own = Module.search_read( |
|
||||
[("name", "=", "module_auto_update")], |
|
||||
["state"], |
|
||||
limit=1) |
|
||||
if own and own[0]["state"] != "uninstalled": |
|
||||
for addon in Module.search([]): |
|
||||
if addon.state != pre_states.get(addon.name): |
|
||||
# Trigger the write hook that should have been |
|
||||
# triggered when the module was [un]installed/updated in |
|
||||
# the limited module graph inside above call to super(), |
|
||||
# and updates its dir checksum as needed |
|
||||
addon.latest_version = addon.latest_version |
|
||||
return result |
|
@ -0,0 +1,84 @@ |
|||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
import logging |
||||
|
|
||||
|
from odoo import api, models |
||||
|
|
||||
|
from ..models.module_deprecated import PARAM_DEPRECATED |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class ModuleUpgrade(models.TransientModel): |
||||
|
_inherit = 'base.module.upgrade' |
||||
|
|
||||
|
@api.model |
||||
|
def _autoupdate_deprecated(self): |
||||
|
"""Know if we should enable deprecated features.""" |
||||
|
deprecated = ( |
||||
|
self.env["ir.config_parameter"].get_param(PARAM_DEPRECATED)) |
||||
|
if deprecated is False: |
||||
|
# Enable deprecated features if this is the 1st automated update |
||||
|
# after the version that deprecated them (X.Y.2.0.0) |
||||
|
own_module = self.env["ir.module.module"].search([ |
||||
|
("name", "=", "module_auto_update"), |
||||
|
]) |
||||
|
try: |
||||
|
if own_module.latest_version.split(".")[2] == "1": |
||||
|
deprecated = "1" |
||||
|
except AttributeError: |
||||
|
pass # 1st install, there's no latest_version |
||||
|
return deprecated == "1" |
||||
|
|
||||
|
@api.model |
||||
|
def get_module_list(self): |
||||
|
"""Set modules to upgrade searching by their dir checksum.""" |
||||
|
if self._autoupdate_deprecated(): |
||||
|
Module = self.env["ir.module.module"] |
||||
|
installed_modules = Module.search([('state', '=', 'installed')]) |
||||
|
upgradeable_modules = installed_modules.filtered( |
||||
|
lambda r: r.checksum_dir != r.checksum_installed, |
||||
|
) |
||||
|
upgradeable_modules.button_upgrade() |
||||
|
return super(ModuleUpgrade, self).get_module_list() |
||||
|
|
||||
|
@api.multi |
||||
|
def upgrade_module(self): |
||||
|
"""Make a fully automated addon upgrade.""" |
||||
|
if self._autoupdate_deprecated(): |
||||
|
_logger.warning( |
||||
|
"You are possibly using an unsupported upgrade system; " |
||||
|
"set '%s' system parameter to '0' and start calling " |
||||
|
"`env['ir.module.module'].upgrade_changed_checksum()` from " |
||||
|
"now on to get rid of this message. See module's README's " |
||||
|
"Known Issues section for further information on the matter." |
||||
|
) |
||||
|
# Compute updates by checksum when called in @api.model fashion |
||||
|
self.env.cr.autocommit(True) # Avoid transaction lock |
||||
|
if not self: |
||||
|
self.get_module_list() |
||||
|
Module = self.env["ir.module.module"] |
||||
|
# Get every addon state before updating |
||||
|
pre_states = {addon["name"]: addon["state"] for addon |
||||
|
in Module.search_read([], ["name", "state"])} |
||||
|
# Perform upgrades, possibly in a limited graph that excludes me |
||||
|
result = super(ModuleUpgrade, self).upgrade_module() |
||||
|
if self._autoupdate_deprecated(): |
||||
|
self.env.cr.autocommit(False) |
||||
|
# Reload environments, anything may have changed |
||||
|
self.env.clear() |
||||
|
# Update addons checksum if state changed and I wasn't uninstalled |
||||
|
own = Module.search_read( |
||||
|
[("name", "=", "module_auto_update")], |
||||
|
["state"], |
||||
|
limit=1) |
||||
|
if own and own[0]["state"] != "uninstalled": |
||||
|
for addon in Module.search([]): |
||||
|
if addon.state != pre_states.get(addon.name): |
||||
|
# Trigger the write hook that should have been |
||||
|
# triggered when the module was [un]installed/updated |
||||
|
# in the limited module graph inside above call to |
||||
|
# super(), and updates its dir checksum as needed |
||||
|
addon.latest_version = addon.latest_version |
||||
|
return result |
Write
Preview
Loading…
Cancel
Save
Reference in new issue