1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (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 General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from os.path import join as opj
35 from osv import fields
42 from zipfile import PyZipFile, ZIP_DEFLATED
43 from cStringIO import StringIO
46 logger = netsvc.Logger()
48 _ad = os.path.abspath(opj(tools.config['root_path'], 'addons')) # default addons path (base)
49 ad = os.path.abspath(tools.config['addons_path']) # alternate addons path
51 sys.path.insert(1, _ad)
53 sys.path.insert(1, ad)
55 # Modules already loaded
61 def addNode(self, name, deps):
62 max_depth, father = 0, None
63 for n in [Node(x, self) for x in deps]:
64 if n.depth >= max_depth:
72 def update_from_db(self, cr):
73 # update the graph with values from the database (if exist)
74 ## First, we set the default values for each package in graph
75 additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
76 ## Then we get the values from the database
77 cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
78 ' FROM ir_module_module'
79 ' WHERE name in (%s)' % (','.join(['%s'] * len(self))),
80 additional_data.keys()
83 ## and we update the default values with values from the database
84 additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
86 for package in self.values():
87 for k, v in additional_data[package.name].items():
88 setattr(package, k, v)
94 done = set(self.keys())
96 level_modules = [(name, module) for name, module in self.items() if module.depth==level]
97 for name, module in level_modules:
102 class Singleton(object):
103 def __new__(cls, name, graph):
107 inst = object.__new__(cls)
113 class Node(Singleton):
115 def __init__(self, name, graph):
117 if not hasattr(self, 'children'):
119 if not hasattr(self, 'depth'):
122 def addChild(self, name):
123 node = Node(name, self.graph)
124 node.depth = self.depth + 1
125 if node not in self.children:
126 self.children.append(node)
127 for attr in ('init', 'update', 'demo'):
128 if hasattr(self, attr):
129 setattr(node, attr, True)
130 self.children.sort(lambda x, y: cmp(x.name, y.name))
132 def __setattr__(self, name, value):
133 super(Singleton, self).__setattr__(name, value)
134 if name in ('init', 'update', 'demo'):
135 tools.config[name][self.name] = 1
136 for child in self.children:
137 setattr(child, name, value)
139 for child in self.children:
140 setattr(child, name, value + 1)
143 return itertools.chain(iter(self.children), *map(iter, self.children))
146 return self._pprint()
148 def _pprint(self, depth=0):
149 s = '%s\n' % self.name
150 for c in self.children:
151 s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1))
155 def get_module_path(module, downloaded=False):
156 """Return the path of the given module."""
157 if os.path.exists(opj(ad, module)) or os.path.exists(opj(ad, '%s.zip' % module)):
158 return opj(ad, module)
160 if os.path.exists(opj(_ad, module)) or os.path.exists(opj(_ad, '%s.zip' % module)):
161 return opj(_ad, module)
163 return opj(_ad, module)
164 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
168 def get_module_filetree(module, dir='.'):
169 path = get_module_path(module)
173 dir = os.path.normpath(dir)
176 if dir.startswith('..') or (dir and dir[0] == '/'):
177 raise Exception('Cannot access file outside the module')
179 if not os.path.isdir(path):
181 zip = zipfile.ZipFile(path + ".zip")
182 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
184 files = tools.osutil.listdir(path, True)
188 if not f.startswith(dir):
192 f = f[len(dir)+int(not dir.endswith('/')):]
193 lst = f.split(os.sep)
196 current = current.setdefault(lst.pop(0), {})
197 current[lst.pop(0)] = None
201 def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=True):
202 """Compress a module directory
204 @param module_directory: The module directory
205 @param base64enc: if True the function will encode the zip file with base64
206 @param src: Integrate the source files
208 @return: a stream to store in a file-like object
211 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
213 def _zippy(archive, path, src=True):
214 path = os.path.abspath(path)
215 base = os.path.basename(path)
216 for f in tools.osutil.listdir(path, True):
217 bf = os.path.basename(f)
218 if not RE_exclude.search(bf) and (src or bf == '__terp__.py' or not bf.endswith('.py')):
219 archive.write(os.path.join(path, f), os.path.join(base, f))
221 archname = StringIO()
222 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
223 archive.writepy(module_directory)
224 _zippy(archive, module_directory, src=src)
226 val = archname.getvalue()
230 val = base64.encodestring(val)
234 def get_module_as_zip(modulename, b64enc=True, src=True):
235 """Generate a module as zip file with the source or not and can do a base64 encoding
237 @param modulename: The module name
238 @param b64enc: if True the function will encode the zip file with base64
239 @param src: Integrate the source files
241 @return: a stream to store in a file-like object
244 ap = get_module_path(str(modulename))
246 raise Exception('Unable to find path for module %s' % modulename)
248 ap = ap.encode('utf8')
249 if os.path.isfile(ap + '.zip'):
250 val = file(ap + '.zip', 'rb').read()
252 val = base64.encodestring(val)
254 val = get_module_as_zip_from_module_directory(ap, b64enc, src)
259 def get_module_resource(module, *args):
260 """Return the full path of a resource of the given module.
262 @param module: the module
263 @param args: the resource path components
265 @return: absolute path to the resource
267 a = get_module_path(module)
268 return a and opj(a, *args) or False
272 """Returns the list of module names
276 name = os.path.basename(name)
277 if name[-4:] == '.zip':
281 def is_really_module(name):
282 name = opj(dir, name)
283 return os.path.isdir(name) or zipfile.is_zipfile(name)
284 return map(clean, filter(is_really_module, os.listdir(dir)))
286 return list(set(listdir(ad) + listdir(_ad)))
288 def get_modules_with_version():
289 modules = get_modules()
291 for module in modules:
292 terp = get_module_resource(module, '__terp__.py')
294 info = eval(tools.file_open(terp).read())
295 res[module] = "%s.%s" % (release.major_version, info['version'])
300 def create_graph(cr, module_list, force=None):
302 upgrade_graph(graph, cr, module_list, force)
305 def upgrade_graph(graph, cr, module_list, force=None):
309 len_graph = len(graph)
310 for module in module_list:
311 mod_path = get_module_path(module)
312 terp_file = get_module_resource(module, '__terp__.py')
313 if not mod_path or not terp_file:
314 cr.execute("update ir_module_module set state=%s where name=%s", ('uninstallable', module))
317 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
319 info = eval(tools.file_open(terp_file).read())
321 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
323 if info.get('installable', True):
324 packages.append((module, info.get('depends', []), info))
326 dependencies = dict([(p, deps) for p, deps, data in packages])
327 current, later = set([p for p, dep, data in packages]), set()
328 while packages and current > later:
329 package, deps, data = packages[0]
331 # if all dependencies of 'package' are already in the graph, add 'package' in the graph
332 if reduce(lambda x, y: x and y in graph, deps, True):
333 if not package in current:
337 current.remove(package)
338 graph.addNode(package, deps)
339 node = Node(package, graph)
341 for kind in ('init', 'demo', 'update'):
342 if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
343 setattr(node, kind, True)
346 packages.append((package, deps, data))
349 graph.update_from_db(cr)
351 for package in later:
352 unmet_deps = filter(lambda p: p not in graph, dependencies[package])
353 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
355 result = len(graph) - len_graph
356 if result != len(module_list):
357 logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
361 def init_module_objects(cr, module_name, obj_list):
362 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name)
365 result = obj._auto_init(cr, {'module': module_name})
368 if hasattr(obj, 'init'):
377 def register_class(m):
379 Register module named m, if not already registered
383 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
384 msg = "Couldn't load %smodule %s" % (mt, m)
385 logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
386 logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
391 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
392 mod_path = get_module_path(m)
395 zip_mod_path = mod_path + '.zip'
396 if not os.path.isfile(zip_mod_path):
397 fm = imp.find_module(m, [ad, _ad])
399 imp.load_module(m, *fm)
404 zimp = zipimport.zipimporter(zip_mod_path)
413 class MigrationManager(object):
415 This class manage the migration of modules
416 Migrations files must be python files containing a "migrate(cr, installed_version)" function.
417 Theses files must respect a directory tree structure: A 'migrations' folder which containt a
418 folder by version. Version can be 'module' version or 'server.module' version (in this case,
419 the files will only be processed by this version of the server). Python file names must start
420 by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
426 | |-- pre-update_table_x.py
427 | |-- pre-update_table_y.py
428 | |-- post-clean-data.py
429 | `-- README.txt # not processed
430 |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server
431 | |-- pre-delete_table_z.py
432 | `-- post-clean-data.py
433 `-- foo.py # not processed
435 This similar structure is generated by the maintenance module with the migrations files get by
436 the maintenance contract
439 def __init__(self, cr, graph):
445 def _get_files(self):
448 import addons.base.maintenance.utils as maintenance_utils
449 maintenance_utils.update_migrations_files(self.cr)
452 for pkg in self.graph:
453 self.migrations[pkg.name] = {}
454 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
457 self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
458 self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
460 def migrate_module(self, pkg, stage):
461 assert stage in ('pre', 'post')
462 stageformat = {'pre': '[>%s]',
466 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
469 def convert_version(version):
470 if version.startswith(release.major_version) and version != release.major_version:
471 return version # the version number already containt the server version
472 return "%s.%s" % (release.major_version, version)
474 def _get_migration_versions(pkg):
476 return [d for d in tree if tree[d] is not None]
479 __get_dir(self.migrations[pkg.name]['module']) +
480 __get_dir(self.migrations[pkg.name]['maintenance'])
482 versions.sort(key=lambda k: parse_version(convert_version(k)))
485 def _get_migration_files(pkg, version, stage):
486 """ return a list of tuple (module, file)
488 m = self.migrations[pkg.name]
491 mapping = {'module': opj(pkg.name, 'migrations'),
492 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
495 for x in mapping.keys():
497 for f in m[x][version]:
498 if m[x][version][f] is not None:
500 if not f.startswith(stage + '-'):
502 lst.append(opj(mapping[x], version, f))
511 from tools.parse_version import parse_version
513 parsed_installed_version = parse_version(pkg.installed_version or '')
514 current_version = parse_version(convert_version(pkg.data.get('version', '0')))
516 versions = _get_migration_versions(pkg)
518 for version in versions:
519 if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
521 strfmt = {'addon': pkg.name,
523 'version': stageformat[stage] % version,
526 for pyfile in _get_migration_files(pkg, version, stage):
527 name, ext = os.path.splitext(os.path.basename(pyfile))
528 if ext.lower() != '.py':
530 mod = fp = fp2 = None
532 fp = tools.file_open(pyfile)
534 # imp.load_source need a real file object, so we create
535 # one from the file-like object we get from file_open
540 mod = imp.load_source(name, pyfile, fp2)
541 logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
542 mod.migrate(self.cr, pkg.installed_version)
544 logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
546 except AttributeError:
547 logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
559 def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
560 # **kwargs is passed directly to convert_xml_import
564 status = status.copy()
567 pool = pooler.get_pool(cr.dbname)
569 migrations = MigrationManager(cr, graph)
574 for package in graph:
575 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
576 migrations.migrate_module(package, 'pre')
577 register_class(package.name)
578 modules = pool.instanciate(package.name, cr)
579 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
580 init_module_objects(cr, package.name, modules)
583 for package in graph:
584 status['progress'] = (float(statusi)+0.1) / len(graph)
590 modobj = pool.get('ir.module.module')
592 if modobj and perform_checks:
593 modobj.check(cr, 1, [mid])
596 status['progress'] = (float(statusi)+0.4) / len(graph)
599 if hasattr(package, 'init') or package.state == 'to install':
602 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
604 for kind in ('init', 'update'):
605 for filename in package.data.get('%s_xml' % kind, []):
606 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
607 name, ext = os.path.splitext(filename)
608 fp = tools.file_open(opj(m, filename))
610 tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode)
612 queries = fp.read().split(';')
613 for query in queries:
614 new_query = ' '.join(query.split())
616 cr.execute(new_query)
618 tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
620 if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
621 status['progress'] = (float(statusi)+0.75) / len(graph)
622 for xml in package.data.get('demo_xml', []):
623 name, ext = os.path.splitext(xml)
624 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
625 fp = tools.file_open(opj(m, xml))
627 tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
629 tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
631 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
632 package_todo.append(package.name)
634 migrations.migrate_module(package, 'post')
637 ver = release.major_version + '.' + package.data.get('version', '1.0')
638 # Set new modules and dependencies
639 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
641 # Update translations for all installed languages
642 modobj.update_translations(cr, 1, [mid], None)
645 package.state = 'installed'
646 for kind in ('init', 'demo', 'update'):
647 if hasattr(package, kind):
648 delattr(package, kind)
652 cr.execute('select model from ir_model where state=%s', ('manual',))
653 for model in cr.dictfetchall():
654 pool.get('ir.model').instanciate(cr, 1, model['model'], {})
656 pool.get('ir.model.data')._process_end(cr, 1, package_todo)
661 def load_modules(db, force_demo=False, status=None, update_module=False):
669 pool = pooler.get_pool(cr.dbname)
671 report = tools.assertion_report()
672 STATES_TO_LOAD = ['installed', 'to upgrade']
673 graph = create_graph(cr, ['base'], force)
676 has_updates = load_module_graph(cr, graph, status, perform_checks=False, report=report)
678 modobj = pool.get('ir.module.module')
679 logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
680 if ('base' in tools.config['init']) or ('base' in tools.config['update']):
681 modobj.update_list(cr, 1)
683 mods = [k for k in tools.config['init'] if tools.config['init'][k]]
685 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
687 modobj.button_install(cr, 1, ids)
689 mods = [k for k in tools.config['update'] if tools.config['update'][k]]
691 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
693 modobj.button_upgrade(cr, 1, ids)
695 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
697 STATES_TO_LOAD += ['to install']
702 if loop_guardrail > 100:
703 raise ProgrammingError()
704 cr.execute("SELECT name from ir_module_module WHERE state in (%s)" % ','.join(['%s']*len(STATES_TO_LOAD)), STATES_TO_LOAD)
706 module_list = [name for (name,) in cr.fetchall() if name not in graph]
710 new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
711 if new_modules_in_graph == 0:
715 logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
716 r = load_module_graph(cr, graph, status, report=report)
717 has_updates = has_updates or r
720 cr.execute("""select model,name from ir_model where id not in (select model_id from ir_model_access)""")
721 for (model, name) in cr.fetchall():
722 logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
724 cr.execute("SELECT model from ir_model")
725 for (model,) in cr.fetchall():
726 obj = pool.get(model)
728 obj._check_removed_columns(cr, log=True)
730 if report.get_report():
731 logger.notifyChannel('init', netsvc.LOG_INFO, report)
733 for kind in ('init', 'demo', 'update'):
734 tools.config[kind] = {}
738 cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
739 for mod_id, mod_name in cr.fetchall():
740 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
741 for rmod, rid in cr.fetchall():
743 pool.get(rmod).unlink(cr, uid, [rid])
744 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
747 # TODO: remove menu without actions of children
750 cr.execute('''delete from
753 (id not in (select parent_id from ir_ui_menu where parent_id is not null))
755 (id not in (select res_id from ir_values where model='ir.ui.menu'))
757 (id not in (select res_id from ir_model_data where model='ir.ui.menu'))''')
762 logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
764 cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
770 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: