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