1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 # Copyright (C) 2010-2011 OpenERP s.a. (<http://openerp.com>).
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 """ Modules migration handling. """
26 from os.path import join as opj
32 import openerp.osv as osv
33 import openerp.tools as tools
34 import openerp.tools.osutil as osutil
35 from openerp.tools.safe_eval import safe_eval as eval
36 from openerp.tools.translate import _
39 import openerp.release as release
43 from zipfile import PyZipFile, ZIP_DEFLATED
44 from cStringIO import StringIO
48 import openerp.modules.db
49 import openerp.modules.graph
51 _logger = logging.getLogger(__name__)
53 class MigrationManager(object):
55 This class manage the migration of modules
56 Migrations files must be python files containing a "migrate(cr, installed_version)" function.
57 Theses files must respect a directory tree structure: A 'migrations' folder which containt a
58 folder by version. Version can be 'module' version or 'server.module' version (in this case,
59 the files will only be processed by this version of the server). Python file names must start
60 by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
66 | |-- pre-update_table_x.py
67 | |-- pre-update_table_y.py
68 | |-- post-clean-data.py
69 | `-- README.txt # not processed
70 |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server
71 | |-- pre-delete_table_z.py
72 | `-- post-clean-data.py
73 `-- foo.py # not processed
75 This similar structure is generated by the maintenance module with the migrations files get by
76 the maintenance contract
79 def __init__(self, cr, graph):
88 import addons.base.maintenance.utils as maintenance_utils
89 maintenance_utils.update_migrations_files(self.cr)
92 for pkg in self.graph:
93 self.migrations[pkg.name] = {}
94 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
97 get_module_filetree = openerp.modules.module.get_module_filetree
98 self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
99 self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
101 def migrate_module(self, pkg, stage):
102 assert stage in ('pre', 'post')
103 stageformat = {'pre': '[>%s]',
107 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
110 def convert_version(version):
111 if version.startswith(release.major_version) and version != release.major_version:
112 return version # the version number already containt the server version
113 return "%s.%s" % (release.major_version, version)
115 def _get_migration_versions(pkg):
117 return [d for d in tree if tree[d] is not None]
120 __get_dir(self.migrations[pkg.name]['module']) +
121 __get_dir(self.migrations[pkg.name]['maintenance'])
123 versions.sort(key=lambda k: parse_version(convert_version(k)))
126 def _get_migration_files(pkg, version, stage):
127 """ return a list of tuple (module, file)
129 m = self.migrations[pkg.name]
132 mapping = {'module': opj(pkg.name, 'migrations'),
133 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
136 for x in mapping.keys():
138 for f in m[x][version]:
139 if m[x][version][f] is not None:
141 if not f.startswith(stage + '-'):
143 lst.append(opj(mapping[x], version, f))
152 from openerp.tools.parse_version import parse_version
154 parsed_installed_version = parse_version(pkg.installed_version or '')
155 current_version = parse_version(convert_version(pkg.data['version']))
157 versions = _get_migration_versions(pkg)
159 for version in versions:
160 if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
162 strfmt = {'addon': pkg.name,
164 'version': stageformat[stage] % version,
167 for pyfile in _get_migration_files(pkg, version, stage):
168 name, ext = os.path.splitext(os.path.basename(pyfile))
169 if ext.lower() != '.py':
171 mod = fp = fp2 = None
173 fp = tools.file_open(pyfile)
175 # imp.load_source need a real file object, so we create
176 # one from the file-like object we get from file_open
181 mod = imp.load_source(name, pyfile, fp2)
182 _logger.info('module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
183 mod.migrate(self.cr, pkg.installed_version)
185 _logger.error('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
187 except AttributeError:
188 _logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
200 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: