[IMP] tests: removed the severity concept from `assert` tags.
[odoo/odoo.git] / openerp / modules / migration.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
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>).
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 ##############################################################################
22
23 """ Modules migration handling. """
24
25 import os, sys, imp
26 from os.path import join as opj
27 import itertools
28 import zipimport
29
30 import openerp
31
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 import openerp.pooler as pooler
37 from openerp.tools.translate import _
38
39 import openerp.netsvc as netsvc
40
41 import zipfile
42 import openerp.release as release
43
44 import re
45 import base64
46 from zipfile import PyZipFile, ZIP_DEFLATED
47 from cStringIO import StringIO
48
49 import logging
50
51 import openerp.modules.db
52 import openerp.modules.graph
53
54 _logger = logging.getLogger(__name__)
55
56 class MigrationManager(object):
57     """
58         This class manage the migration of modules
59         Migrations files must be python files containing a "migrate(cr, installed_version)" function.
60         Theses files must respect a directory tree structure: A 'migrations' folder which containt a
61         folder by version. Version can be 'module' version or 'server.module' version (in this case,
62         the files will only be processed by this version of the server). Python file names must start
63         by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
64         Example:
65
66             <moduledir>
67             `-- migrations
68                 |-- 1.0
69                 |   |-- pre-update_table_x.py
70                 |   |-- pre-update_table_y.py
71                 |   |-- post-clean-data.py
72                 |   `-- README.txt              # not processed
73                 |-- 5.0.1.1                     # files in this folder will be executed only on a 5.0 server
74                 |   |-- pre-delete_table_z.py
75                 |   `-- post-clean-data.py
76                 `-- foo.py                      # not processed
77
78         This similar structure is generated by the maintenance module with the migrations files get by
79         the maintenance contract
80
81     """
82     def __init__(self, cr, graph):
83         self.cr = cr
84         self.graph = graph
85         self.migrations = {}
86         self._get_files()
87
88     def _get_files(self):
89
90         """
91         import addons.base.maintenance.utils as maintenance_utils
92         maintenance_utils.update_migrations_files(self.cr)
93         #"""
94
95         for pkg in self.graph:
96             self.migrations[pkg.name] = {}
97             if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
98                 continue
99
100             get_module_filetree = openerp.modules.module.get_module_filetree
101             self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
102             self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
103
104     def migrate_module(self, pkg, stage):
105         assert stage in ('pre', 'post')
106         stageformat = {'pre': '[>%s]',
107                        'post': '[%s>]',
108                       }
109
110         if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
111             return
112
113         def convert_version(version):
114             if version.startswith(release.major_version) and version != release.major_version:
115                 return version  # the version number already containt the server version
116             return "%s.%s" % (release.major_version, version)
117
118         def _get_migration_versions(pkg):
119             def __get_dir(tree):
120                 return [d for d in tree if tree[d] is not None]
121
122             versions = list(set(
123                 __get_dir(self.migrations[pkg.name]['module']) +
124                 __get_dir(self.migrations[pkg.name]['maintenance'])
125             ))
126             versions.sort(key=lambda k: parse_version(convert_version(k)))
127             return versions
128
129         def _get_migration_files(pkg, version, stage):
130             """ return a list of tuple (module, file)
131             """
132             m = self.migrations[pkg.name]
133             lst = []
134
135             mapping = {'module': opj(pkg.name, 'migrations'),
136                        'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
137                       }
138
139             for x in mapping.keys():
140                 if version in m[x]:
141                     for f in m[x][version]:
142                         if m[x][version][f] is not None:
143                             continue
144                         if not f.startswith(stage + '-'):
145                             continue
146                         lst.append(opj(mapping[x], version, f))
147             lst.sort()
148             return lst
149
150         def mergedict(a, b):
151             a = a.copy()
152             a.update(b)
153             return a
154
155         from openerp.tools.parse_version import parse_version
156
157         parsed_installed_version = parse_version(pkg.installed_version or '')
158         current_version = parse_version(convert_version(pkg.data['version']))
159
160         versions = _get_migration_versions(pkg)
161
162         for version in versions:
163             if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
164
165                 strfmt = {'addon': pkg.name,
166                           'stage': stage,
167                           'version': stageformat[stage] % version,
168                           }
169
170                 for pyfile in _get_migration_files(pkg, version, stage):
171                     name, ext = os.path.splitext(os.path.basename(pyfile))
172                     if ext.lower() != '.py':
173                         continue
174                     mod = fp = fp2 = None
175                     try:
176                         fp = tools.file_open(pyfile)
177
178                         # imp.load_source need a real file object, so we create
179                         # one from the file-like object we get from file_open
180                         fp2 = os.tmpfile()
181                         fp2.write(fp.read())
182                         fp2.seek(0)
183                         try:
184                             mod = imp.load_source(name, pyfile, fp2)
185                             _logger.info('module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
186                             mod.migrate(self.cr, pkg.installed_version)
187                         except ImportError:
188                             _logger.error('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
189                             raise
190                         except AttributeError:
191                             _logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
192                         except:
193                             raise
194                     finally:
195                         if fp:
196                             fp.close()
197                         if fp2:
198                             fp2.close()
199                         if mod:
200                             del mod
201
202
203 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: