[MERGE] Merge from trunk-wms-loconopreport-jco
[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 from openerp.tools.translate import _
37
38 import zipfile
39 import openerp.release as release
40
41 import re
42 import base64
43 from zipfile import PyZipFile, ZIP_DEFLATED
44 from cStringIO import StringIO
45
46 import logging
47
48 import openerp.modules.db
49 import openerp.modules.graph
50
51 _logger = logging.getLogger(__name__)
52
53 class MigrationManager(object):
54     """
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
61         Example:
62
63             <moduledir>
64             `-- migrations
65                 |-- 1.0
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
74
75         This similar structure is generated by the maintenance module with the migrations files get by
76         the maintenance contract
77
78     """
79     def __init__(self, cr, graph):
80         self.cr = cr
81         self.graph = graph
82         self.migrations = {}
83         self._get_files()
84
85     def _get_files(self):
86
87         """
88         import addons.base.maintenance.utils as maintenance_utils
89         maintenance_utils.update_migrations_files(self.cr)
90         #"""
91
92         for pkg in self.graph:
93             self.migrations[pkg.name] = {}
94             if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
95                 continue
96
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 {}
100
101     def migrate_module(self, pkg, stage):
102         assert stage in ('pre', 'post')
103         stageformat = {'pre': '[>%s]',
104                        'post': '[%s>]',
105                       }
106
107         if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
108             return
109
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)
114
115         def _get_migration_versions(pkg):
116             def __get_dir(tree):
117                 return [d for d in tree if tree[d] is not None]
118
119             versions = list(set(
120                 __get_dir(self.migrations[pkg.name]['module']) +
121                 __get_dir(self.migrations[pkg.name]['maintenance'])
122             ))
123             versions.sort(key=lambda k: parse_version(convert_version(k)))
124             return versions
125
126         def _get_migration_files(pkg, version, stage):
127             """ return a list of tuple (module, file)
128             """
129             m = self.migrations[pkg.name]
130             lst = []
131
132             mapping = {'module': opj(pkg.name, 'migrations'),
133                        'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
134                       }
135
136             for x in mapping.keys():
137                 if version in m[x]:
138                     for f in m[x][version]:
139                         if m[x][version][f] is not None:
140                             continue
141                         if not f.startswith(stage + '-'):
142                             continue
143                         lst.append(opj(mapping[x], version, f))
144             lst.sort()
145             return lst
146
147         def mergedict(a, b):
148             a = a.copy()
149             a.update(b)
150             return a
151
152         from openerp.tools.parse_version import parse_version
153
154         parsed_installed_version = parse_version(pkg.installed_version or '')
155         current_version = parse_version(convert_version(pkg.data['version']))
156
157         versions = _get_migration_versions(pkg)
158
159         for version in versions:
160             if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
161
162                 strfmt = {'addon': pkg.name,
163                           'stage': stage,
164                           'version': stageformat[stage] % version,
165                           }
166
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':
170                         continue
171                     mod = fp = fp2 = None
172                     try:
173                         fp = tools.file_open(pyfile)
174
175                         # imp.load_source need a real file object, so we create
176                         # one from the file-like object we get from file_open
177                         fp2 = os.tmpfile()
178                         fp2.write(fp.read())
179                         fp2.seek(0)
180                         try:
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)
184                         except ImportError:
185                             _logger.error('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
186                             raise
187                         except AttributeError:
188                             _logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
189                         except:
190                             raise
191                     finally:
192                         if fp:
193                             fp.close()
194                         if fp2:
195                             fp2.close()
196                         if mod:
197                             del mod
198
199
200 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: