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