[MERGE] safe_eval branch merged: plain eval() replaced by safer version wherever...
[odoo/odoo.git] / bin / addons / __init__.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 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 import os, sys, imp
24 from os.path import join as opj
25 import itertools
26 import zipimport
27
28 import osv
29 import tools
30 import tools.osutil
31 from tools.safe_eval import safe_eval as eval
32 import pooler
33
34
35 import netsvc
36 from osv import fields
37 import addons
38
39 import zipfile
40 import release
41
42 import re
43 import base64
44 from zipfile import PyZipFile, ZIP_DEFLATED
45 from cStringIO import StringIO
46
47 import logging
48
49
50 logger = netsvc.Logger()
51
52 _ad = os.path.abspath(opj(tools.config['root_path'], 'addons'))     # default addons path (base)
53 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
54
55 sys.path.insert(1, _ad)
56
57 ad_cnt=1
58 for adp in ad_paths:
59     if adp != _ad:
60         sys.path.insert(ad_cnt, adp)
61         ad_cnt+=1
62
63 ad_paths.append(_ad)    # for get_module_path
64
65 # Modules already loaded
66 loaded = []
67
68 #Modules whch raised error
69 not_loaded = []
70
71 class Graph(dict):
72
73     def addNode(self, name, deps):
74         max_depth, father = 0, None
75         for n in [Node(x, self) for x in deps]:
76             if n.depth >= max_depth:
77                 father = n
78                 max_depth = n.depth
79         if father:
80             father.addChild(name)
81         else:
82             Node(name, self)
83
84     def update_from_db(self, cr):
85         # update the graph with values from the database (if exist)
86         ## First, we set the default values for each package in graph
87         additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
88         ## Then we get the values from the database
89         cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
90                    '  FROM ir_module_module'
91                    ' WHERE name in (%s)' % (','.join(['%s'] * len(self))),
92                     additional_data.keys()
93                    )
94
95         ## and we update the default values with values from the database
96         additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
97
98         for package in self.values():
99             for k, v in additional_data[package.name].items():
100                 setattr(package, k, v)
101
102
103
104     def __iter__(self):
105         level = 0
106         done = set(self.keys())
107         while done:
108             level_modules = [(name, module) for name, module in self.items() if module.depth==level]
109             for name, module in level_modules:
110                 done.remove(name)
111                 yield module
112             level += 1
113
114 class Singleton(object):
115     def __new__(cls, name, graph):
116         if name in graph:
117             inst = graph[name]
118         else:
119             inst = object.__new__(cls)
120             inst.name = name
121             graph[name] = inst
122         return inst
123
124
125 class Node(Singleton):
126
127     def __init__(self, name, graph):
128         self.graph = graph
129         if not hasattr(self, 'children'):
130             self.children = []
131         if not hasattr(self, 'depth'):
132             self.depth = 0
133
134     def addChild(self, name):
135         node = Node(name, self.graph)
136         node.depth = self.depth + 1
137         if node not in self.children:
138             self.children.append(node)
139         for attr in ('init', 'update', 'demo'):
140             if hasattr(self, attr):
141                 setattr(node, attr, True)
142         self.children.sort(lambda x, y: cmp(x.name, y.name))
143
144     def __setattr__(self, name, value):
145         super(Singleton, self).__setattr__(name, value)
146         if name in ('init', 'update', 'demo'):
147             tools.config[name][self.name] = 1
148             for child in self.children:
149                 setattr(child, name, value)
150         if name == 'depth':
151             for child in self.children:
152                 setattr(child, name, value + 1)
153
154     def __iter__(self):
155         return itertools.chain(iter(self.children), *map(iter, self.children))
156
157     def __str__(self):
158         return self._pprint()
159
160     def _pprint(self, depth=0):
161         s = '%s\n' % self.name
162         for c in self.children:
163             s += '%s`-> %s' % ('   ' * depth, c._pprint(depth+1))
164         return s
165
166
167 def get_module_path(module, downloaded=False):
168     """Return the path of the given module."""
169     for adp in ad_paths:
170         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
171             return opj(adp, module)
172
173     if downloaded:
174         return opj(_ad, module)
175     logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
176     return False
177
178
179 def get_module_filetree(module, dir='.'):
180     path = get_module_path(module)
181     if not path:
182         return False
183
184     dir = os.path.normpath(dir)
185     if dir == '.':
186         dir = ''
187     if dir.startswith('..') or (dir and dir[0] == '/'):
188         raise Exception('Cannot access file outside the module')
189
190     if not os.path.isdir(path):
191         # zipmodule
192         zip = zipfile.ZipFile(path + ".zip")
193         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
194     else:
195         files = tools.osutil.listdir(path, True)
196
197     tree = {}
198     for f in files:
199         if not f.startswith(dir):
200             continue
201
202         if dir:
203             f = f[len(dir)+int(not dir.endswith('/')):]
204         lst = f.split(os.sep)
205         current = tree
206         while len(lst) != 1:
207             current = current.setdefault(lst.pop(0), {})
208         current[lst.pop(0)] = None
209
210     return tree
211
212 def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=True):
213     """Compress a module directory
214
215     @param module_directory: The module directory
216     @param base64enc: if True the function will encode the zip file with base64
217     @param src: Integrate the source files
218
219     @return: a stream to store in a file-like object
220     """
221
222     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
223
224     def _zippy(archive, path, src=True):
225         path = os.path.abspath(path)
226         base = os.path.basename(path)
227         for f in tools.osutil.listdir(path, True):
228             bf = os.path.basename(f)
229             if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
230                 archive.write(os.path.join(path, f), os.path.join(base, f))
231
232     archname = StringIO()
233     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
234     archive.writepy(module_directory)
235     _zippy(archive, module_directory, src=src)
236     archive.close()
237     val = archname.getvalue()
238     archname.close()
239
240     if b64enc:
241         val = base64.encodestring(val)
242
243     return val
244
245 def get_module_as_zip(modulename, b64enc=True, src=True):
246     """Generate a module as zip file with the source or not and can do a base64 encoding
247
248     @param modulename: The module name
249     @param b64enc: if True the function will encode the zip file with base64
250     @param src: Integrate the source files
251
252     @return: a stream to store in a file-like object
253     """
254
255     ap = get_module_path(str(modulename))
256     if not ap:
257         raise Exception('Unable to find path for module %s' % modulename)
258
259     ap = ap.encode('utf8')
260     if os.path.isfile(ap + '.zip'):
261         val = file(ap + '.zip', 'rb').read()
262         if b64enc:
263             val = base64.encodestring(val)
264     else:
265         val = get_module_as_zip_from_module_directory(ap, b64enc, src)
266
267     return val
268
269
270 def get_module_resource(module, *args):
271     """Return the full path of a resource of the given module.
272
273     @param module: the module
274     @param args: the resource path components
275
276     @return: absolute path to the resource
277     """
278     a = get_module_path(module)
279     return a and opj(a, *args) or False
280
281
282 def get_modules():
283     """Returns the list of module names
284     """
285     def listdir(dir):
286         def clean(name):
287             name = os.path.basename(name)
288             if name[-4:] == '.zip':
289                 name = name[:-4]
290             return name
291
292         def is_really_module(name):
293             name = opj(dir, name)
294             return os.path.isdir(name) or zipfile.is_zipfile(name)
295         return map(clean, filter(is_really_module, os.listdir(dir)))
296
297     plist = []
298     for ad in ad_paths:
299         plist.extend(listdir(ad))
300     return list(set(plist))
301
302 def load_information_from_description_file(module):
303     """
304     :param module: The name of the module (sale, purchase, ...)
305     """
306     for filename in ['__openerp__.py', '__terp__.py']:
307         description_file = addons.get_module_resource(module, filename)
308         if os.path.isfile(description_file):
309             return eval(tools.file_open(description_file).read())
310
311     #TODO: refactor the logger in this file to follow the logging guidelines
312     #      for 6.0
313     logging.getLogger('addons').debug('The module %s does not contain a description file:'\
314                                       '__openerp__.py or __terp__.py (deprecated)', module)
315     return {}
316
317 def get_modules_with_version():
318     modules = get_modules()
319     res = {}
320     for module in modules:
321         try:
322             info = load_information_from_description_file(module)
323             res[module] = "%s.%s" % (release.major_version, info['version'])
324         except Exception, e:
325             continue
326     return res
327
328 def create_graph(cr, module_list, force=None):
329     graph = Graph()
330     upgrade_graph(graph, cr, module_list, force)
331     return graph
332
333 def upgrade_graph(graph, cr, module_list, force=None):
334     if force is None:
335         force = []
336     packages = []
337     len_graph = len(graph)
338     for module in module_list:
339         mod_path = get_module_path(module)
340         terp_file = get_module_resource(module, '__openerp__.py')
341         if not terp_file or not os.path.isfile(terp_file):
342             terp_file = get_module_resource(module, '__terp__.py')
343
344         if not mod_path or not terp_file:
345             global not_loaded
346             not_loaded.append(module)
347             logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable' % (module))
348             raise osv.osv.except_osv('Error!',"Module '%s' was not found" % (module,))
349
350
351         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
352             try:
353                 info = eval(tools.file_open(terp_file).read())
354             except:
355                 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
356                 raise
357             if info.get('installable', True):
358                 packages.append((module, info.get('depends', []), info))
359
360
361     dependencies = dict([(p, deps) for p, deps, data in packages])
362     current, later = set([p for p, dep, data in packages]), set()
363
364     while packages and current > later:
365         package, deps, data = packages[0]
366
367         # if all dependencies of 'package' are already in the graph, add 'package' in the graph
368         if reduce(lambda x, y: x and y in graph, deps, True):
369             if not package in current:
370                 packages.pop(0)
371                 continue
372             later.clear()
373             current.remove(package)
374             graph.addNode(package, deps)
375             node = Node(package, graph)
376             node.data = data
377             for kind in ('init', 'demo', 'update'):
378                 if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
379                     setattr(node, kind, True)
380         else:
381             later.add(package)
382             packages.append((package, deps, data))
383         packages.pop(0)
384
385     graph.update_from_db(cr)
386
387     for package in later:
388         unmet_deps = filter(lambda p: p not in graph, dependencies[package])
389         logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
390
391     result = len(graph) - len_graph
392     if result != len(module_list):
393         logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
394     return result
395
396
397 def init_module_objects(cr, module_name, obj_list):
398     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name)
399     todo = []
400     for obj in obj_list:
401         try:
402             result = obj._auto_init(cr, {'module': module_name})
403         except Exception, e:
404             raise
405         if result:
406             todo += result
407         if hasattr(obj, 'init'):
408             obj.init(cr)
409         cr.commit()
410     todo.sort()
411     for t in todo:
412         t[1](cr, *t[2])
413     cr.commit()
414
415
416 def register_class(m):
417     """
418     Register module named m, if not already registered
419     """
420
421     def log(e):
422         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
423         msg = "Couldn't load %smodule %s" % (mt, m)
424         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
425         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
426
427     global loaded
428     if m in loaded:
429         return
430     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
431     mod_path = get_module_path(m)
432
433     try:
434         zip_mod_path = mod_path + '.zip'
435         if not os.path.isfile(zip_mod_path):
436             fm = imp.find_module(m, ad_paths)
437             try:
438                 imp.load_module(m, *fm)
439             finally:
440                 if fm[0]:
441                     fm[0].close()
442         else:
443             zimp = zipimport.zipimporter(zip_mod_path)
444             zimp.load_module(m)
445     except Exception, e:
446         log(e)
447         raise
448     else:
449         loaded.append(m)
450
451
452 class MigrationManager(object):
453     """
454         This class manage the migration of modules
455         Migrations files must be python files containing a "migrate(cr, installed_version)" function.
456         Theses files must respect a directory tree structure: A 'migrations' folder which containt a
457         folder by version. Version can be 'module' version or 'server.module' version (in this case,
458         the files will only be processed by this version of the server). Python file names must start
459         by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
460         Example:
461
462             <moduledir>
463             `-- migrations
464                 |-- 1.0
465                 |   |-- pre-update_table_x.py
466                 |   |-- pre-update_table_y.py
467                 |   |-- post-clean-data.py
468                 |   `-- README.txt              # not processed
469                 |-- 5.0.1.1                     # files in this folder will be executed only on a 5.0 server
470                 |   |-- pre-delete_table_z.py
471                 |   `-- post-clean-data.py
472                 `-- foo.py                      # not processed
473
474         This similar structure is generated by the maintenance module with the migrations files get by
475         the maintenance contract
476
477     """
478     def __init__(self, cr, graph):
479         self.cr = cr
480         self.graph = graph
481         self.migrations = {}
482         self._get_files()
483
484     def _get_files(self):
485
486         """
487         import addons.base.maintenance.utils as maintenance_utils
488         maintenance_utils.update_migrations_files(self.cr)
489         #"""
490
491         for pkg in self.graph:
492             self.migrations[pkg.name] = {}
493             if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
494                 continue
495
496             self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
497             self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
498
499     def migrate_module(self, pkg, stage):
500         assert stage in ('pre', 'post')
501         stageformat = {'pre': '[>%s]',
502                        'post': '[%s>]',
503                       }
504
505         if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
506             return
507
508         def convert_version(version):
509             if version.startswith(release.major_version) and version != release.major_version:
510                 return version  # the version number already containt the server version
511             return "%s.%s" % (release.major_version, version)
512
513         def _get_migration_versions(pkg):
514             def __get_dir(tree):
515                 return [d for d in tree if tree[d] is not None]
516
517             versions = list(set(
518                 __get_dir(self.migrations[pkg.name]['module']) +
519                 __get_dir(self.migrations[pkg.name]['maintenance'])
520             ))
521             versions.sort(key=lambda k: parse_version(convert_version(k)))
522             return versions
523
524         def _get_migration_files(pkg, version, stage):
525             """ return a list of tuple (module, file)
526             """
527             m = self.migrations[pkg.name]
528             lst = []
529
530             mapping = {'module': opj(pkg.name, 'migrations'),
531                        'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
532                       }
533
534             for x in mapping.keys():
535                 if version in m[x]:
536                     for f in m[x][version]:
537                         if m[x][version][f] is not None:
538                             continue
539                         if not f.startswith(stage + '-'):
540                             continue
541                         lst.append(opj(mapping[x], version, f))
542             lst.sort()
543             return lst
544
545         def mergedict(a, b):
546             a = a.copy()
547             a.update(b)
548             return a
549
550         from tools.parse_version import parse_version
551
552         parsed_installed_version = parse_version(pkg.installed_version or '')
553         current_version = parse_version(convert_version(pkg.data.get('version', '0')))
554
555         versions = _get_migration_versions(pkg)
556
557         for version in versions:
558             if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
559
560                 strfmt = {'addon': pkg.name,
561                           'stage': stage,
562                           'version': stageformat[stage] % version,
563                           }
564
565                 for pyfile in _get_migration_files(pkg, version, stage):
566                     name, ext = os.path.splitext(os.path.basename(pyfile))
567                     if ext.lower() != '.py':
568                         continue
569                     mod = fp = fp2 = None
570                     try:
571                         fp = tools.file_open(pyfile)
572
573                         # imp.load_source need a real file object, so we create
574                         # one from the file-like object we get from file_open
575                         fp2 = os.tmpfile()
576                         fp2.write(fp.read())
577                         fp2.seek(0)
578                         try:
579                             mod = imp.load_source(name, pyfile, fp2)
580                             logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
581                             mod.migrate(self.cr, pkg.installed_version)
582                         except ImportError:
583                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
584                             raise
585                         except AttributeError:
586                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
587                         except:
588                             raise
589                     finally:
590                         if fp:
591                             fp.close()
592                         if fp2:
593                             fp2.close()
594                         if mod:
595                             del mod
596
597 log = logging.getLogger('init')
598
599 def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
600
601     def process_sql_file(cr, file):
602         queries = fp.read().split(';')
603         for query in queries:
604             new_query = ' '.join(query.split())
605             if new_query:
606                 cr.execute(new_query)
607
608     def load_init_update_xml(cr, m, idref, mode, kind):
609         for filename in package.data.get('%s_xml' % kind, []):
610             logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
611             _, ext = os.path.splitext(filename)
612             fp = tools.file_open(opj(m, filename))
613             if ext == '.csv':
614                 noupdate = (kind == 'init')
615                 tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate)
616             elif ext == '.sql':
617                 process_sql_file(cr, fp)
618             elif ext == '.yml':
619                 tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs)
620             else:
621                 tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
622             fp.close()
623
624     def load_demo_xml(cr, m, idref, mode):
625         for xml in package.data.get('demo_xml', []):
626             name, ext = os.path.splitext(xml)
627             logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
628             fp = tools.file_open(opj(m, xml))
629             if ext == '.csv':
630                 tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
631             elif ext == '.yml':
632                 tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
633             else:
634                 tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
635             fp.close()
636
637     def load_data(cr, module_name, id_map, mode):
638         _load_data(cr, module_name, id_map, mode, 'data')
639
640     def load_demo(cr, module_name, id_map, mode):
641         _load_data(cr, module_name, id_map, mode, 'demo')
642
643     def load_test(cr, module_name, id_map, mode):
644         cr.commit()
645         if not tools.config.options['test-disable']:
646             try:
647                 _load_data(cr, module_name, id_map, mode, 'test')
648             except Exception, e:
649                 logger.notifyChannel('ERROR', netsvc.LOG_TEST, e)
650                 pass
651             finally:
652                 if tools.config.options['test-commit']:
653                     cr.commit()
654                 else:
655                     cr.rollback()
656
657     def _load_data(cr, module_name, id_map, mode, kind):
658         noupdate = (kind == 'demo')
659         for filename in package.data.get(kind, []):
660             _, ext = os.path.splitext(filename)
661             log.info("module %s: loading %s", module_name, filename)
662             pathname = os.path.join(module_name, filename)
663             file = tools.file_open(pathname)
664             # TODO manage .csv file with noupdate == (kind == 'init')
665             if ext == '.sql':
666                 process_sql_file(cr, fp)
667             elif ext == '.yml':
668                 tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate)
669             else:
670                 tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate)
671             file.close()
672
673     # **kwargs is passed directly to convert_xml_import
674     if not status:
675         status = {}
676
677     status = status.copy()
678     package_todo = []
679     statusi = 0
680     pool = pooler.get_pool(cr.dbname)
681
682     migrations = MigrationManager(cr, graph)
683
684     has_updates = False
685     modobj = None
686
687     logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
688
689     for package in graph:
690         logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
691         migrations.migrate_module(package, 'pre')
692         register_class(package.name)
693         modules = pool.instanciate(package.name, cr)
694         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
695             init_module_objects(cr, package.name, modules)
696         cr.commit()
697
698     for package in graph:
699         status['progress'] = (float(statusi)+0.1) / len(graph)
700         m = package.name
701         mid = package.id
702
703         if modobj is None:
704             modobj = pool.get('ir.module.module')
705
706         if modobj and perform_checks:
707             modobj.check(cr, 1, [mid])
708
709         idref = {}
710         status['progress'] = (float(statusi)+0.4) / len(graph)
711
712         mode = 'update'
713         if hasattr(package, 'init') or package.state == 'to install':
714             mode = 'init'
715
716         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
717             has_updates = True
718             for kind in ('init', 'update'):
719                 if package.state=='to upgrade':
720                     # upgrading the module information
721                     modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data))
722                 load_init_update_xml(cr, m, idref, mode, kind)
723             load_data(cr, m, idref, mode)
724             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
725                 status['progress'] = (float(statusi)+0.75) / len(graph)
726                 load_demo_xml(cr, m, idref, mode)
727                 load_demo(cr, m, idref, mode)
728                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
729
730             load_test(cr, m, idref, mode)
731
732             package_todo.append(package.name)
733
734             migrations.migrate_module(package, 'post')
735
736             if modobj:
737                 ver = release.major_version + '.' + package.data.get('version', '1.0')
738                 # Set new modules and dependencies
739                 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
740                 cr.commit()
741                 # Update translations for all installed languages
742                 modobj.update_translations(cr, 1, [mid], None)
743                 cr.commit()
744
745             package.state = 'installed'
746             for kind in ('init', 'demo', 'update'):
747                 if hasattr(package, kind):
748                     delattr(package, kind)
749
750         statusi += 1
751
752     cr.execute('select model from ir_model where state=%s', ('manual',))
753     for model in cr.dictfetchall():
754         pool.get('ir.model').instanciate(cr, 1, model['model'], {})
755
756     pool.get('ir.model.data')._process_end(cr, 1, package_todo)
757     cr.commit()
758
759     return has_updates
760
761 def load_modules(db, force_demo=False, status=None, update_module=False):
762
763     def check_module_name(cr, mods, state):
764         for mod in mods:
765             id = modobj.search(cr, 1, ['&', ('state', '=', state), ('name', '=', mod)])
766             if id:
767                 getattr(modobj, states[state])(cr, 1, id)
768             elif mod != 'all':
769                 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: invalid module name!' % (mod))
770
771     if not status:
772         status = {}
773     cr = db.cursor()
774     if cr:
775         cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'")
776         if len(cr.fetchall())==0:
777             logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
778             tools.init_db(cr)
779             tools.config["init"]["all"] = 1
780             tools.config['update']['all'] = 1
781             if not tools.config['without_demo']:
782                 tools.config["demo"]['all'] = 1
783     force = []
784     if force_demo:
785         force.append('demo')
786     pool = pooler.get_pool(cr.dbname)
787     try:
788         report = tools.assertion_report()
789         # NOTE: Try to also load the modules that have been marked as uninstallable previously...
790         STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
791         graph = create_graph(cr, ['base'], force)
792         has_updates = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
793
794         global not_loaded
795         if not_loaded:
796             #If some module is not loaded don't proceed further
797             not_loaded = []
798             return
799         if update_module:
800             modobj = pool.get('ir.module.module')
801             states = {'installed': 'button_upgrade', 'uninstalled': 'button_install'}
802             logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
803             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
804                 modobj.update_list(cr, 1)
805
806             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
807             check_module_name(cr, mods, 'uninstalled')
808
809             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
810             check_module_name(cr, mods, 'installed')
811
812             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
813
814             STATES_TO_LOAD += ['to install']
815
816         loop_guardrail = 0
817         while True:
818             loop_guardrail += 1
819             if loop_guardrail > 100:
820                 raise ProgrammingError()
821             cr.execute("SELECT name from ir_module_module WHERE state in (%s)" % ','.join(['%s']*len(STATES_TO_LOAD)), STATES_TO_LOAD)
822
823             module_list = [name for (name,) in cr.fetchall() if name not in graph]
824             if not module_list:
825                 break
826
827             new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
828             if new_modules_in_graph == 0:
829                 # nothing to load
830                 break
831
832             logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
833             r = load_module_graph(cr, graph, status, report=report)
834             has_updates = has_updates or r
835
836         if has_updates:
837             cr.execute("""select model,name from ir_model where id not in (select model_id from ir_model_access)""")
838             for (model, name) in cr.fetchall():
839                 logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
840
841             cr.execute("SELECT model from ir_model")
842             for (model,) in cr.fetchall():
843                 obj = pool.get(model)
844                 if obj:
845                     obj._check_removed_columns(cr, log=True)
846
847         if report.get_report():
848             logger.notifyChannel('init', netsvc.LOG_INFO, report)
849
850         for kind in ('init', 'demo', 'update'):
851             tools.config[kind] = {}
852
853         cr.commit()
854         if update_module:
855             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
856             for mod_id, mod_name in cr.fetchall():
857                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
858                 for rmod, rid in cr.fetchall():
859                     uid = 1
860                     rmod_module= pool.get(rmod)
861                     if rmod_module:
862                         rmod_module.unlink(cr, uid, [rid])
863                     else:
864                         logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
865                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
866                 cr.commit()
867             #
868             # TODO: remove menu without actions of children
869             #
870             while True:
871                 cr.execute('''delete from
872                         ir_ui_menu
873                     where
874                         (id not in (select parent_id from ir_ui_menu where parent_id is not null))
875                     and
876                         (id not in (select res_id from ir_values where model='ir.ui.menu'))
877                     and
878                         (id not in (select res_id from ir_model_data where model='ir.ui.menu'))''')
879                 cr.commit()
880                 if not cr.rowcount:
881                     break
882                 else:
883                     logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
884
885             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
886             cr.commit()
887     finally:
888         cr.close()
889
890
891 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: