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