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_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
50 sys.path.insert(1, _ad)
55 sys.path.insert(ad_cnt, adp)
58 ad_paths.append(_ad) # for get_module_path
60 # Modules already loaded
63 #Modules whch raised error
68 def addNode(self, name, deps):
69 max_depth, father = 0, None
70 for n in [Node(x, self) for x in deps]:
71 if n.depth >= max_depth:
79 def update_from_db(self, cr):
80 # update the graph with values from the database (if exist)
81 ## First, we set the default values for each package in graph
82 additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
83 ## Then we get the values from the database
84 cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
85 ' FROM ir_module_module'
86 ' WHERE name in (%s)' % (','.join(['%s'] * len(self))),
87 additional_data.keys()
90 ## and we update the default values with values from the database
91 additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
93 for package in self.values():
94 for k, v in additional_data[package.name].items():
95 setattr(package, k, v)
101 done = set(self.keys())
103 level_modules = [(name, module) for name, module in self.items() if module.depth==level]
104 for name, module in level_modules:
109 class Singleton(object):
110 def __new__(cls, name, graph):
114 inst = object.__new__(cls)
120 class Node(Singleton):
122 def __init__(self, name, graph):
124 if not hasattr(self, 'children'):
126 if not hasattr(self, 'depth'):
129 def addChild(self, name):
130 node = Node(name, self.graph)
131 node.depth = self.depth + 1
132 if node not in self.children:
133 self.children.append(node)
134 for attr in ('init', 'update', 'demo'):
135 if hasattr(self, attr):
136 setattr(node, attr, True)
137 self.children.sort(lambda x, y: cmp(x.name, y.name))
139 def __setattr__(self, name, value):
140 super(Singleton, self).__setattr__(name, value)
141 if name in ('init', 'update', 'demo'):
142 tools.config[name][self.name] = 1
143 for child in self.children:
144 setattr(child, name, value)
146 for child in self.children:
147 setattr(child, name, value + 1)
150 return itertools.chain(iter(self.children), *map(iter, self.children))
153 return self._pprint()
155 def _pprint(self, depth=0):
156 s = '%s\n' % self.name
157 for c in self.children:
158 s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1))
162 def get_module_path(module, downloaded=False):
163 """Return the path of the given module."""
165 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
166 return opj(adp, module)
169 return opj(_ad, module)
170 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
174 def get_module_filetree(module, dir='.'):
175 path = get_module_path(module)
179 dir = os.path.normpath(dir)
182 if dir.startswith('..') or (dir and dir[0] == '/'):
183 raise Exception('Cannot access file outside the module')
185 if not os.path.isdir(path):
187 zip = zipfile.ZipFile(path + ".zip")
188 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
190 files = tools.osutil.listdir(path, True)
194 if not f.startswith(dir):
198 f = f[len(dir)+int(not dir.endswith('/')):]
199 lst = f.split(os.sep)
202 current = current.setdefault(lst.pop(0), {})
203 current[lst.pop(0)] = None
207 def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=True):
208 """Compress a module directory
210 @param module_directory: The module directory
211 @param base64enc: if True the function will encode the zip file with base64
212 @param src: Integrate the source files
214 @return: a stream to store in a file-like object
217 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
219 def _zippy(archive, path, src=True):
220 path = os.path.abspath(path)
221 base = os.path.basename(path)
222 for f in tools.osutil.listdir(path, True):
223 bf = os.path.basename(f)
224 if not RE_exclude.search(bf) and (src or bf == '__terp__.py' or not bf.endswith('.py')):
225 archive.write(os.path.join(path, f), os.path.join(base, f))
227 archname = StringIO()
228 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
229 archive.writepy(module_directory)
230 _zippy(archive, module_directory, src=src)
232 val = archname.getvalue()
236 val = base64.encodestring(val)
240 def get_module_as_zip(modulename, b64enc=True, src=True):
241 """Generate a module as zip file with the source or not and can do a base64 encoding
243 @param modulename: The module name
244 @param b64enc: if True the function will encode the zip file with base64
245 @param src: Integrate the source files
247 @return: a stream to store in a file-like object
250 ap = get_module_path(str(modulename))
252 raise Exception('Unable to find path for module %s' % modulename)
254 ap = ap.encode('utf8')
255 if os.path.isfile(ap + '.zip'):
256 val = file(ap + '.zip', 'rb').read()
258 val = base64.encodestring(val)
260 val = get_module_as_zip_from_module_directory(ap, b64enc, src)
265 def get_module_resource(module, *args):
266 """Return the full path of a resource of the given module.
268 @param module: the module
269 @param args: the resource path components
271 @return: absolute path to the resource
273 a = get_module_path(module)
274 return a and opj(a, *args) or False
278 """Returns the list of module names
282 name = os.path.basename(name)
283 if name[-4:] == '.zip':
287 def is_really_module(name):
288 name = opj(dir, name)
289 return os.path.isdir(name) or zipfile.is_zipfile(name)
290 return map(clean, filter(is_really_module, os.listdir(dir)))
294 plist.extend(listdir(ad))
295 return list(set(plist))
297 def get_modules_with_version():
298 modules = get_modules()
300 for module in modules:
301 terp = get_module_resource(module, '__terp__.py')
303 info = eval(tools.file_open(terp).read())
304 res[module] = "%s.%s" % (release.major_version, info['version'])
309 def create_graph(cr, module_list, force=None):
311 upgrade_graph(graph, cr, module_list, force)
314 def upgrade_graph(graph, cr, module_list, force=None):
318 len_graph = len(graph)
319 for module in module_list:
320 mod_path = get_module_path(module)
321 terp_file = get_module_resource(module, '__terp__.py')
322 if not mod_path or not terp_file:
324 not_loaded.append(module)
325 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable' % (module))
326 raise osv.osv.except_osv('Error!',"Module '%s' was not found" % (module,))
328 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
330 info = eval(tools.file_open(terp_file).read())
332 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
334 if info.get('installable', True):
335 packages.append((module, info.get('depends', []), info))
338 dependencies = dict([(p, deps) for p, deps, data in packages])
339 current, later = set([p for p, dep, data in packages]), set()
341 while packages and current > later:
342 package, deps, data = packages[0]
344 # if all dependencies of 'package' are already in the graph, add 'package' in the graph
345 if reduce(lambda x, y: x and y in graph, deps, True):
346 if not package in current:
350 current.remove(package)
351 graph.addNode(package, deps)
352 node = Node(package, graph)
354 for kind in ('init', 'demo', 'update'):
355 if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
356 setattr(node, kind, True)
359 packages.append((package, deps, data))
362 graph.update_from_db(cr)
364 for package in later:
365 unmet_deps = filter(lambda p: p not in graph, dependencies[package])
366 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
368 result = len(graph) - len_graph
369 if result != len(module_list):
370 logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
374 def init_module_objects(cr, module_name, obj_list):
375 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name)
379 result = obj._auto_init(cr, {'module': module_name})
384 if hasattr(obj, 'init'):
393 def register_class(m):
395 Register module named m, if not already registered
399 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
400 msg = "Couldn't load %smodule %s" % (mt, m)
401 logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
402 logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
407 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
408 mod_path = get_module_path(m)
411 zip_mod_path = mod_path + '.zip'
412 if not os.path.isfile(zip_mod_path):
413 fm = imp.find_module(m, ad_paths)
415 imp.load_module(m, *fm)
420 zimp = zipimport.zipimporter(zip_mod_path)
429 class MigrationManager(object):
431 This class manage the migration of modules
432 Migrations files must be python files containing a "migrate(cr, installed_version)" function.
433 Theses files must respect a directory tree structure: A 'migrations' folder which containt a
434 folder by version. Version can be 'module' version or 'server.module' version (in this case,
435 the files will only be processed by this version of the server). Python file names must start
436 by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
442 | |-- pre-update_table_x.py
443 | |-- pre-update_table_y.py
444 | |-- post-clean-data.py
445 | `-- README.txt # not processed
446 |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server
447 | |-- pre-delete_table_z.py
448 | `-- post-clean-data.py
449 `-- foo.py # not processed
451 This similar structure is generated by the maintenance module with the migrations files get by
452 the maintenance contract
455 def __init__(self, cr, graph):
461 def _get_files(self):
464 import addons.base.maintenance.utils as maintenance_utils
465 maintenance_utils.update_migrations_files(self.cr)
468 for pkg in self.graph:
469 self.migrations[pkg.name] = {}
470 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
473 self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
474 self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
476 def migrate_module(self, pkg, stage):
477 assert stage in ('pre', 'post')
478 stageformat = {'pre': '[>%s]',
482 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
485 def convert_version(version):
486 if version.startswith(release.major_version) and version != release.major_version:
487 return version # the version number already containt the server version
488 return "%s.%s" % (release.major_version, version)
490 def _get_migration_versions(pkg):
492 return [d for d in tree if tree[d] is not None]
495 __get_dir(self.migrations[pkg.name]['module']) +
496 __get_dir(self.migrations[pkg.name]['maintenance'])
498 versions.sort(key=lambda k: parse_version(convert_version(k)))
501 def _get_migration_files(pkg, version, stage):
502 """ return a list of tuple (module, file)
504 m = self.migrations[pkg.name]
507 mapping = {'module': opj(pkg.name, 'migrations'),
508 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
511 for x in mapping.keys():
513 for f in m[x][version]:
514 if m[x][version][f] is not None:
516 if not f.startswith(stage + '-'):
518 lst.append(opj(mapping[x], version, f))
527 from tools.parse_version import parse_version
529 parsed_installed_version = parse_version(pkg.installed_version or '')
530 current_version = parse_version(convert_version(pkg.data.get('version', '0')))
532 versions = _get_migration_versions(pkg)
534 for version in versions:
535 if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
537 strfmt = {'addon': pkg.name,
539 'version': stageformat[stage] % version,
542 for pyfile in _get_migration_files(pkg, version, stage):
543 name, ext = os.path.splitext(os.path.basename(pyfile))
544 if ext.lower() != '.py':
546 mod = fp = fp2 = None
548 fp = tools.file_open(pyfile)
550 # imp.load_source need a real file object, so we create
551 # one from the file-like object we get from file_open
556 mod = imp.load_source(name, pyfile, fp2)
557 logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
558 mod.migrate(self.cr, pkg.installed_version)
560 logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
562 except AttributeError:
563 logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
575 def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
576 # **kwargs is passed directly to convert_xml_import
580 status = status.copy()
583 pool = pooler.get_pool(cr.dbname)
585 migrations = MigrationManager(cr, graph)
590 logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
592 for package in graph:
593 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
594 migrations.migrate_module(package, 'pre')
595 register_class(package.name)
596 modules = pool.instanciate(package.name, cr)
597 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
598 init_module_objects(cr, package.name, modules)
601 for package in graph:
602 status['progress'] = (float(statusi)+0.1) / len(graph)
607 modobj = pool.get('ir.module.module')
609 if modobj and perform_checks:
610 modobj.check(cr, 1, [mid])
613 status['progress'] = (float(statusi)+0.4) / len(graph)
616 if hasattr(package, 'init') or package.state == 'to install':
619 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
621 for kind in ('init', 'update'):
622 if package.state=='to upgrade':
623 # upgrading the module information
624 modobj.write(cr, 1, [mid], {
625 'description': package.data.get('description', ''),
626 'shortdesc': package.data.get('name', ''),
627 'author': package.data.get('author', 'Unknown'),
628 'website': package.data.get('website', ''),
629 'license': package.data.get('license', 'GPL-2'),
630 'certificate': package.data.get('certificate') or None,
632 for filename in package.data.get('%s_xml' % kind, []):
633 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
634 name, ext = os.path.splitext(filename)
635 fp = tools.file_open(opj(m, filename))
640 tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate)
642 queries = fp.read().split(';')
643 for query in queries:
644 new_query = ' '.join(query.split())
646 cr.execute(new_query)
648 tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs)
650 tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
652 if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
653 status['progress'] = (float(statusi)+0.75) / len(graph)
654 for xml in package.data.get('demo_xml', []):
655 name, ext = os.path.splitext(xml)
656 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
657 fp = tools.file_open(opj(m, xml))
659 tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
661 tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs)
663 tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
665 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
666 package_todo.append(package.name)
668 migrations.migrate_module(package, 'post')
671 ver = release.major_version + '.' + package.data.get('version', '1.0')
672 # Set new modules and dependencies
673 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
675 # Update translations for all installed languages
676 modobj.update_translations(cr, 1, [mid], None)
679 package.state = 'installed'
680 for kind in ('init', 'demo', 'update'):
681 if hasattr(package, kind):
682 delattr(package, kind)
686 cr.execute('select model from ir_model where state=%s', ('manual',))
687 for model in cr.dictfetchall():
688 pool.get('ir.model').instanciate(cr, 1, model['model'], {})
690 pool.get('ir.model.data')._process_end(cr, 1, package_todo)
695 def load_modules(db, force_demo=False, status=None, update_module=False):
700 cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'")
701 if len(cr.fetchall())==0:
702 logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
704 # cr.execute("update res_users set password=%s where id=%s",('admin',1))
705 # in that case, force --init=all
706 tools.config["init"]["all"] = 1
707 tools.config['update']['all'] = 1
708 if not tools.config['without_demo']:
709 tools.config["demo"]['all'] = 1
713 pool = pooler.get_pool(cr.dbname)
715 report = tools.assertion_report()
716 # NOTE: Try to also load the modules that have been marked as uninstallable previously...
717 STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
718 graph = create_graph(cr, ['base'], force)
720 has_updates = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
724 #If some module is not loaded don't proceed further
728 modobj = pool.get('ir.module.module')
729 logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
730 if ('base' in tools.config['init']) or ('base' in tools.config['update']):
731 modobj.update_list(cr, 1)
733 mods = [k for k in tools.config['init'] if tools.config['init'][k]]
735 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
737 modobj.button_install(cr, 1, ids)
739 mods = [k for k in tools.config['update'] if tools.config['update'][k]]
741 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
743 modobj.button_upgrade(cr, 1, ids)
745 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
747 STATES_TO_LOAD += ['to install']
752 if loop_guardrail > 100:
753 raise ProgrammingError()
754 cr.execute("SELECT name from ir_module_module WHERE state in (%s)" % ','.join(['%s']*len(STATES_TO_LOAD)), STATES_TO_LOAD)
756 module_list = [name for (name,) in cr.fetchall() if name not in graph]
760 new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
761 if new_modules_in_graph == 0:
764 logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
765 r = load_module_graph(cr, graph, status, report=report)
766 has_updates = has_updates or r
769 cr.execute("""select model,name from ir_model where id not in (select model_id from ir_model_access)""")
770 for (model, name) in cr.fetchall():
771 logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
773 cr.execute("SELECT model from ir_model")
774 for (model,) in cr.fetchall():
775 obj = pool.get(model)
777 obj._check_removed_columns(cr, log=True)
779 if report.get_report():
780 logger.notifyChannel('init', netsvc.LOG_INFO, report)
782 for kind in ('init', 'demo', 'update'):
783 tools.config[kind] = {}
787 cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
788 for mod_id, mod_name in cr.fetchall():
789 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
790 for rmod, rid in cr.fetchall():
792 rmod_module= pool.get(rmod)
794 rmod_module.unlink(cr, uid, [rid])
796 logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
797 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
800 # TODO: remove menu without actions of children
803 cr.execute('''delete from
806 (id not in (select parent_id from ir_ui_menu where parent_id is not null))
808 (id not in (select res_id from ir_values where model='ir.ui.menu'))
810 (id not in (select res_id from ir_model_data where model='ir.ui.menu'))''')
815 logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
817 cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
823 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: