checking if there is an installed version set is not enough because this field is not reset when the module is uninstalled
182 lines
7.3 KiB
Python
182 lines
7.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
|
# Copyright (C) 2010-2014 OpenERP s.a. (<http://openerp.com>).
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
|
|
""" Modules migration handling. """
|
|
|
|
import imp
|
|
import logging
|
|
import os
|
|
from os.path import join as opj
|
|
|
|
import openerp
|
|
import openerp.release as release
|
|
import openerp.tools as tools
|
|
from openerp.tools.parse_version import parse_version
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class MigrationManager(object):
|
|
"""
|
|
This class manage the migration of modules
|
|
Migrations files must be python files containing a "migrate(cr, installed_version)" function.
|
|
Theses files must respect a directory tree structure: A 'migrations' folder which containt a
|
|
folder by version. Version can be 'module' version or 'server.module' version (in this case,
|
|
the files will only be processed by this version of the server). Python file names must start
|
|
by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
|
|
Example:
|
|
|
|
<moduledir>
|
|
`-- migrations
|
|
|-- 1.0
|
|
| |-- pre-update_table_x.py
|
|
| |-- pre-update_table_y.py
|
|
| |-- post-clean-data.py
|
|
| `-- README.txt # not processed
|
|
|-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server
|
|
| |-- pre-delete_table_z.py
|
|
| `-- post-clean-data.py
|
|
`-- foo.py # not processed
|
|
|
|
This similar structure is generated by the maintenance module with the migrations files get by
|
|
the maintenance contract
|
|
|
|
"""
|
|
def __init__(self, cr, graph):
|
|
self.cr = cr
|
|
self.graph = graph
|
|
self.migrations = {}
|
|
self._get_files()
|
|
|
|
def _get_files(self):
|
|
|
|
"""
|
|
import addons.base.maintenance.utils as maintenance_utils
|
|
maintenance_utils.update_migrations_files(self.cr)
|
|
#"""
|
|
|
|
for pkg in self.graph:
|
|
self.migrations[pkg.name] = {}
|
|
if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
|
|
continue
|
|
|
|
get_module_filetree = openerp.modules.module.get_module_filetree
|
|
self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
|
|
self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
|
|
|
|
def migrate_module(self, pkg, stage):
|
|
assert stage in ('pre', 'post')
|
|
stageformat = {
|
|
'pre': '[>%s]',
|
|
'post': '[%s>]',
|
|
}
|
|
|
|
if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade') or pkg.state == 'to install':
|
|
return
|
|
|
|
def convert_version(version):
|
|
if version.count('.') >= 2:
|
|
return version # the version number already containt the server version
|
|
return "%s.%s" % (release.major_version, version)
|
|
|
|
def _get_migration_versions(pkg):
|
|
def __get_dir(tree):
|
|
return [d for d in tree if tree[d] is not None]
|
|
|
|
versions = list(set(
|
|
__get_dir(self.migrations[pkg.name]['module']) +
|
|
__get_dir(self.migrations[pkg.name]['maintenance'])
|
|
))
|
|
versions.sort(key=lambda k: parse_version(convert_version(k)))
|
|
return versions
|
|
|
|
def _get_migration_files(pkg, version, stage):
|
|
""" return a list of tuple (module, file)
|
|
"""
|
|
m = self.migrations[pkg.name]
|
|
lst = []
|
|
|
|
mapping = {
|
|
'module': opj(pkg.name, 'migrations'),
|
|
'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
|
|
}
|
|
|
|
for x in mapping.keys():
|
|
if version in m[x]:
|
|
for f in m[x][version]:
|
|
if m[x][version][f] is not None:
|
|
continue
|
|
if not f.startswith(stage + '-'):
|
|
continue
|
|
lst.append(opj(mapping[x], version, f))
|
|
lst.sort()
|
|
return lst
|
|
|
|
parsed_installed_version = parse_version(pkg.installed_version or '')
|
|
current_version = parse_version(convert_version(pkg.data['version']))
|
|
|
|
versions = _get_migration_versions(pkg)
|
|
|
|
for version in versions:
|
|
if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
|
|
|
|
strfmt = {'addon': pkg.name,
|
|
'stage': stage,
|
|
'version': stageformat[stage] % version,
|
|
}
|
|
|
|
for pyfile in _get_migration_files(pkg, version, stage):
|
|
name, ext = os.path.splitext(os.path.basename(pyfile))
|
|
if ext.lower() != '.py':
|
|
continue
|
|
mod = fp = fp2 = None
|
|
try:
|
|
fp, fname = tools.file_open(pyfile, pathinfo=True)
|
|
|
|
if not isinstance(fp, file):
|
|
# imp.load_source need a real file object, so we create
|
|
# one from the file-like object we get from file_open
|
|
fp2 = os.tmpfile()
|
|
fp2.write(fp.read())
|
|
fp2.seek(0)
|
|
try:
|
|
mod = imp.load_source(name, fname, fp2 or fp)
|
|
_logger.info('module %(addon)s: Running migration %(version)s %(name)s' % dict(strfmt, name=mod.__name__))
|
|
migrate = mod.migrate
|
|
except ImportError:
|
|
_logger.exception('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % dict(strfmt, file=pyfile))
|
|
raise
|
|
except AttributeError:
|
|
_logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
|
|
else:
|
|
migrate(self.cr, pkg.installed_version)
|
|
finally:
|
|
if fp:
|
|
fp.close()
|
|
if fp2:
|
|
fp2.close()
|
|
if mod:
|
|
del mod
|
|
|
|
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|