1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from os.path import join as opj
34 from osv import fields
41 from zipfile import PyZipFile, ZIP_DEFLATED
42 from cStringIO import StringIO
45 logger = netsvc.Logger()
47 _ad = os.path.abspath(opj(tools.config['root_path'], 'addons')) # default addons path (base)
48 ad = os.path.abspath(tools.config['addons_path']) # alternate addons path
50 sys.path.insert(1, _ad)
52 sys.path.insert(1, ad)
54 # Modules already loaded
60 def addNode(self, name, deps):
61 max_depth, father = 0, None
62 for n in [Node(x, self) for x in deps]:
63 if n.depth >= max_depth:
71 def update_from_db(self, cr):
72 # update the graph with values from the database (if exist)
73 ## First, we set the default values for each package in graph
74 additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
75 ## Then we get the values from the database
76 cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
77 ' FROM ir_module_module'
78 ' WHERE name in (%s)' % (','.join(['%s'] * len(self))),
79 additional_data.keys()
82 ## and we update the default values with values from the database
83 additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
85 for package in self.values():
86 for k, v in additional_data[package.name].items():
87 setattr(package, k, v)
93 done = set(self.keys())
95 level_modules = [(name, module) for name, module in self.items() if module.depth==level]
96 for name, module in level_modules:
101 class Singleton(object):
102 def __new__(cls, name, graph):
106 inst = object.__new__(cls)
112 class Node(Singleton):
114 def __init__(self, name, graph):
116 if not hasattr(self, 'children'):
118 if not hasattr(self, 'depth'):
121 def addChild(self, name):
122 node = Node(name, self.graph)
123 node.depth = self.depth + 1
124 if node not in self.children:
125 self.children.append(node)
126 for attr in ('init', 'update', 'demo'):
127 if hasattr(self, attr):
128 setattr(node, attr, True)
129 self.children.sort(lambda x, y: cmp(x.name, y.name))
131 def __setattr__(self, name, value):
132 super(Singleton, self).__setattr__(name, value)
133 if name in ('init', 'update', 'demo'):
134 tools.config[name][self.name] = 1
135 for child in self.children:
136 setattr(child, name, value)
138 for child in self.children:
139 setattr(child, name, value + 1)
142 return itertools.chain(iter(self.children), *map(iter, self.children))
145 return self._pprint()
147 def _pprint(self, depth=0):
148 s = '%s\n' % self.name
149 for c in self.children:
150 s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1))
154 def get_module_path(module, downloaded=False):
155 """Return the path of the given module."""
156 if os.path.exists(opj(ad, module)) or os.path.exists(opj(ad, '%s.zip' % module)):
157 return opj(ad, module)
159 if os.path.exists(opj(_ad, module)) or os.path.exists(opj(_ad, '%s.zip' % module)):
160 return opj(_ad, module)
162 return opj(_ad, module)
163 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
167 def get_module_filetree(module, dir='.'):
168 path = get_module_path(module)
172 dir = os.path.normpath(dir)
175 if dir.startswith('..') or (dir and dir[0] == '/'):
176 raise Exception('Cannot access file outside the module')
178 if not os.path.isdir(path):
180 zip = zipfile.ZipFile(path + ".zip")
181 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
183 files = tools.osutil.listdir(path, True)
187 if not f.startswith(dir):
191 f = f[len(dir)+int(not dir.endswith('/')):]
192 lst = f.split(os.sep)
195 current = current.setdefault(lst.pop(0), {})
196 current[lst.pop(0)] = None
200 def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=True):
201 """Compress a module directory
203 @param module_directory: The module directory
204 @param base64enc: if True the function will encode the zip file with base64
205 @param src: Integrate the source files
207 @return: a stream to store in a file-like object
210 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
212 def _zippy(archive, path, src=True):
213 path = os.path.abspath(path)
214 base = os.path.basename(path)
215 for f in tools.osutil.listdir(path, True):
216 bf = os.path.basename(f)
217 if not RE_exclude.search(bf) and (src or bf == '__terp__.py' or not bf.endswith('.py')):
218 archive.write(os.path.join(path, f), os.path.join(base, f))
220 archname = StringIO()
221 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
222 archive.writepy(module_directory)
223 _zippy(archive, module_directory, src=src)
225 val = archname.getvalue()
229 val = base64.encodestring(val)
233 def get_module_as_zip(modulename, b64enc=True, src=True):
234 """Generate a module as zip file with the source or not and can do a base64 encoding
236 @param modulename: The module name
237 @param b64enc: if True the function will encode the zip file with base64
238 @param src: Integrate the source files
240 @return: a stream to store in a file-like object
243 ap = get_module_path(str(modulename))
245 raise Exception('Unable to find path for module %s' % modulename)
247 ap = ap.encode('utf8')
248 if os.path.isfile(ap + '.zip'):
249 val = file(ap + '.zip', 'rb').read()
251 val = base64.encodestring(val)
253 val = get_module_as_zip_from_module_directory(ap, b64enc, src)
258 def get_module_resource(module, *args):
259 """Return the full path of a resource of the given module.
261 @param module: the module
262 @param args: the resource path components
264 @return: absolute path to the resource
266 a = get_module_path(module)
267 return a and opj(a, *args) or False
271 """Returns the list of module names
275 name = os.path.basename(name)
276 if name[-4:] == '.zip':
280 def is_really_module(name):
281 name = opj(dir, name)
282 return os.path.isdir(name) or zipfile.is_zipfile(name)
283 return map(clean, filter(is_really_module, os.listdir(dir)))
285 return list(set(listdir(ad) + listdir(_ad)))
287 def get_modules_with_version():
288 modules = get_modules()
290 for module in modules:
291 terp = get_module_resource(module, '__terp__.py')
293 info = eval(tools.file_open(terp).read())
294 res[module] = "%s.%s" % (release.major_version, info['version'])
299 def create_graph(cr, module_list, force=None):
301 upgrade_graph(graph, cr, module_list, force)
304 def upgrade_graph(graph, cr, module_list, force=None):
308 len_graph = len(graph)
309 for module in module_list:
310 mod_path = get_module_path(module)
311 terp_file = get_module_resource(module, '__terp__.py')
312 if not mod_path or not terp_file:
313 cr.execute("update ir_module_module set state=%s where name=%s", ('uninstallable', module))
316 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
318 info = eval(tools.file_open(terp_file).read())
320 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
322 if info.get('installable', True):
323 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)
589 modobj = pool.get('ir.module.module')
591 if modobj and perform_checks:
592 modobj.check(cr, 1, [mid])
595 status['progress'] = (float(statusi)+0.4) / len(graph)
598 if hasattr(package, 'init') or package.state == 'to install':
601 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
603 for kind in ('init', 'update'):
604 for filename in package.data.get('%s_xml' % kind, []):
605 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
606 name, ext = os.path.splitext(filename)
607 fp = tools.file_open(opj(m, filename))
609 tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode)
611 queries = fp.read().split(';')
612 for query in queries:
613 new_query = ' '.join(query.split())
615 cr.execute(new_query)
617 tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
619 if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
620 status['progress'] = (float(statusi)+0.75) / len(graph)
621 for xml in package.data.get('demo_xml', []):
622 name, ext = os.path.splitext(xml)
623 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
624 fp = tools.file_open(opj(m, xml))
626 tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
628 tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
630 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
631 package_todo.append(package.name)
633 migrations.migrate_module(package, 'post')
636 ver = release.major_version + '.' + package.data.get('version', '1.0')
637 # Set new modules and dependencies
638 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
640 # Update translations for all installed languages
641 modobj.update_translations(cr, 1, [mid], None)
644 package.state = 'installed'
645 for kind in ('init', 'demo', 'update'):
646 if hasattr(package, kind):
647 delattr(package, kind)
651 cr.execute('select model from ir_model where state=%s', ('manual',))
652 for model in cr.dictfetchall():
653 pool.get('ir.model').instanciate(cr, 1, model['model'], {})
655 pool.get('ir.model.data')._process_end(cr, 1, package_todo)
660 def load_modules(db, force_demo=False, status=None, update_module=False):
666 cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'")
667 if len(cr.fetchall())==0:
668 logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
670 # cr.execute("update res_users set password=%s where id=%s",('admin',1))
671 # in that case, force --init=all
672 tools.config["init"]["all"] = 1
673 tools.config['update']['all'] = 1
674 if not tools.config['without_demo']:
675 tools.config["demo"]['all'] = 1
679 pool = pooler.get_pool(cr.dbname)
681 report = tools.assertion_report()
682 # NOTE: Try to also load the modules that have been marked as uninstallable previously...
683 STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
684 graph = create_graph(cr, ['base'], force)
686 has_updates = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
689 modobj = pool.get('ir.module.module')
690 logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
691 if ('base' in tools.config['init']) or ('base' in tools.config['update']):
692 modobj.update_list(cr, 1)
694 mods = [k for k in tools.config['init'] if tools.config['init'][k]]
696 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
698 modobj.button_install(cr, 1, ids)
700 mods = [k for k in tools.config['update'] if tools.config['update'][k]]
702 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
704 modobj.button_upgrade(cr, 1, ids)
706 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
708 STATES_TO_LOAD += ['to install']
713 if loop_guardrail > 100:
714 raise ProgrammingError()
715 cr.execute("SELECT name from ir_module_module WHERE state in (%s)" % ','.join(['%s']*len(STATES_TO_LOAD)), STATES_TO_LOAD)
717 module_list = [name for (name,) in cr.fetchall() if name not in graph]
721 new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
722 if new_modules_in_graph == 0:
726 logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
727 r = load_module_graph(cr, graph, status, report=report)
728 has_updates = has_updates or r
731 cr.execute("""select model,name from ir_model where id not in (select model_id from ir_model_access)""")
732 for (model, name) in cr.fetchall():
733 logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
735 cr.execute("SELECT model from ir_model")
736 for (model,) in cr.fetchall():
737 obj = pool.get(model)
739 obj._check_removed_columns(cr, log=True)
741 if report.get_report():
742 logger.notifyChannel('init', netsvc.LOG_INFO, report)
744 for kind in ('init', 'demo', 'update'):
745 tools.config[kind] = {}
749 cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
750 for mod_id, mod_name in cr.fetchall():
751 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
752 for rmod, rid in cr.fetchall():
754 pool.get(rmod).unlink(cr, uid, [rid])
755 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
758 # TODO: remove menu without actions of children
761 cr.execute('''delete from
764 (id not in (select parent_id from ir_ui_menu where parent_id is not null))
766 (id not in (select res_id from ir_values where model='ir.ui.menu'))
768 (id not in (select res_id from ir_model_data where model='ir.ui.menu'))''')
773 logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
775 cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
781 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: