[FIX] Dependancies loop executes when you install a new module
[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-2011 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 from tools.translate import _
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.ustr(tools.config['root_path']), u'addons'))     # default addons path (base)
51 ad_paths= map(lambda m: os.path.abspath(tools.ustr(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 zip_directory(directory, b64enc=True, src=True):
207     """Compress a directory
208
209     @param directory: The directory to compress
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 string containing the zip file
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
229     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
230     directory = tools.ustr(directory).encode('utf-8')
231
232     archive.writepy(directory)
233     _zippy(archive, directory, src=src)
234     archive.close()
235     archive_data = archname.getvalue()
236     archname.close()
237
238     if b64enc:
239         return base64.encodestring(archive_data)
240
241     return archive_data
242
243 def get_module_as_zip(modulename, b64enc=True, src=True):
244     """Generate a module as zip file with the source or not and can do a base64 encoding
245
246     @param modulename: The module name
247     @param b64enc: if True the function will encode the zip file with base64
248     @param src: Integrate the source files
249
250     @return: a stream to store in a file-like object
251     """
252
253     ap = get_module_path(str(modulename))
254     if not ap:
255         raise Exception('Unable to find path for module %s' % modulename)
256
257     ap = ap.encode('utf8')
258     if os.path.isfile(ap + '.zip'):
259         val = file(ap + '.zip', 'rb').read()
260         if b64enc:
261             val = base64.encodestring(val)
262     else:
263         val = zip_directory(ap, b64enc, src)
264
265     return val
266
267
268 def get_module_resource(module, *args):
269     """Return the full path of a resource of the given module.
270
271     @param module: the module
272     @param args: the resource path components
273
274     @return: absolute path to the resource
275     """
276     a = get_module_path(module)
277     if not a: return False
278     resource_path = opj(a, *args)
279     if zipfile.is_zipfile( a +'.zip') :
280         zip = zipfile.ZipFile( a + ".zip")
281         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
282         resource_path = '/'.join(args)
283         if resource_path in files:
284             return opj(a, resource_path)
285     elif os.path.exists(resource_path):
286         return resource_path
287     return False
288
289 def get_modules():
290     """Returns the list of module names
291     """
292     def listdir(dir):
293         def clean(name):
294             name = os.path.basename(name)
295             if name[-4:] == '.zip':
296                 name = name[:-4]
297             return name
298
299         def is_really_module(name):
300             name = opj(dir, name)
301             return os.path.isdir(name) or zipfile.is_zipfile(name)
302         return map(clean, filter(is_really_module, os.listdir(dir)))
303
304     plist = []
305     for ad in ad_paths:
306         plist.extend(listdir(ad))
307     return list(set(plist))
308
309 def load_information_from_description_file(module):
310     """
311     :param module: The name of the module (sale, purchase, ...)
312     """
313
314     for filename in ['__openerp__.py', '__terp__.py']:
315         description_file = get_module_resource(module, filename)
316         if description_file :
317             desc_f = tools.file_open(description_file)
318             try:
319                 return eval(desc_f.read())
320             finally:
321                 desc_f.close()
322
323     #TODO: refactor the logger in this file to follow the logging guidelines
324     #      for 6.0
325     logging.getLogger('addons').debug('The module %s does not contain a description file:'\
326                                       '__openerp__.py or __terp__.py (deprecated)', module)
327     return {}
328
329 def get_modules_with_version():
330     modules = get_modules()
331     res = {}
332     for module in modules:
333         try:
334             info = load_information_from_description_file(module)
335             res[module] = "%s.%s" % (release.major_version, info['version'])
336         except Exception, e:
337             continue
338     return res
339
340 def create_graph(cr, module_list, force=None):
341     graph = Graph()
342     upgrade_graph(graph, cr, module_list, force)
343     return graph
344
345 def upgrade_graph(graph, cr, module_list, force=None):
346     if force is None:
347         force = []
348     packages = []
349     len_graph = len(graph)
350     for module in module_list:
351         mod_path = get_module_path(module)
352         terp_file = get_module_resource(module, '__openerp__.py')
353         if not terp_file:
354             terp_file = get_module_resource(module, '__terp__.py')
355         if not mod_path or not terp_file:
356             logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not found, skipped' % (module))
357             continue
358
359         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
360             terp_f = tools.file_open(terp_file)
361             try:
362                 info = eval(terp_f.read())
363             except Exception:
364                 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
365                 raise
366             finally:
367                 terp_f.close()
368             if info.get('installable', True):
369                 packages.append((module, info.get('depends', []), info))
370             else:
371                 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module))
372
373     dependencies = dict([(p, deps) for p, deps, data in packages])
374     current, later = set([p for p, dep, data in packages]), set()
375
376     while packages and current > later:
377         package, deps, data = packages[0]
378
379         # if all dependencies of 'package' are already in the graph, add 'package' in the graph
380         if reduce(lambda x, y: x and y in graph, deps, True):
381             if not package in current:
382                 packages.pop(0)
383                 continue
384             later.clear()
385             current.remove(package)
386             graph.addNode(package, deps)
387             node = Node(package, graph)
388             node.data = data
389             for kind in ('init', 'demo', 'update'):
390                 if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
391                     setattr(node, kind, True)
392         else:
393             later.add(package)
394             packages.append((package, deps, data))
395         packages.pop(0)
396
397     graph.update_from_db(cr)
398
399     for package in later:
400         unmet_deps = filter(lambda p: p not in graph, dependencies[package])
401         logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
402
403     result = len(graph) - len_graph
404     if result != len(module_list):
405         logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
406     return result
407
408
409 def init_module_objects(cr, module_name, obj_list):
410     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name)
411     todo = []
412     for obj in obj_list:
413         try:
414             result = obj._auto_init(cr, {'module': module_name})
415         except Exception, e:
416             raise
417         if result:
418             todo += result
419         if hasattr(obj, 'init'):
420             obj.init(cr)
421         cr.commit()
422     todo.sort()
423     for t in todo:
424         t[1](cr, *t[2])
425     cr.commit()
426
427
428 def register_class(m):
429     """
430     Register module named m, if not already registered
431     """
432
433     def log(e):
434         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
435         msg = "Couldn't load %smodule %s" % (mt, m)
436         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
437         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
438
439     global loaded
440     if m in loaded:
441         return
442     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
443     mod_path = get_module_path(m)
444
445     try:
446         zip_mod_path = mod_path + '.zip'
447         if not os.path.isfile(zip_mod_path):
448             fm = imp.find_module(m, ad_paths)
449             try:
450                 imp.load_module(m, *fm)
451             finally:
452                 if fm[0]:
453                     fm[0].close()
454         else:
455             zimp = zipimport.zipimporter(zip_mod_path)
456             zimp.load_module(m)
457     except Exception, e:
458         log(e)
459         raise
460     else:
461         loaded.append(m)
462
463
464 class MigrationManager(object):
465     """
466         This class manage the migration of modules
467         Migrations files must be python files containing a "migrate(cr, installed_version)" function.
468         Theses files must respect a directory tree structure: A 'migrations' folder which containt a
469         folder by version. Version can be 'module' version or 'server.module' version (in this case,
470         the files will only be processed by this version of the server). Python file names must start
471         by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
472         Example:
473
474             <moduledir>
475             `-- migrations
476                 |-- 1.0
477                 |   |-- pre-update_table_x.py
478                 |   |-- pre-update_table_y.py
479                 |   |-- post-clean-data.py
480                 |   `-- README.txt              # not processed
481                 |-- 5.0.1.1                     # files in this folder will be executed only on a 5.0 server
482                 |   |-- pre-delete_table_z.py
483                 |   `-- post-clean-data.py
484                 `-- foo.py                      # not processed
485
486         This similar structure is generated by the maintenance module with the migrations files get by
487         the maintenance contract
488
489     """
490     def __init__(self, cr, graph):
491         self.cr = cr
492         self.graph = graph
493         self.migrations = {}
494         self._get_files()
495
496     def _get_files(self):
497
498         """
499         import addons.base.maintenance.utils as maintenance_utils
500         maintenance_utils.update_migrations_files(self.cr)
501         #"""
502
503         for pkg in self.graph:
504             self.migrations[pkg.name] = {}
505             if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
506                 continue
507
508             self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
509             self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
510
511     def migrate_module(self, pkg, stage):
512         assert stage in ('pre', 'post')
513         stageformat = {'pre': '[>%s]',
514                        'post': '[%s>]',
515                       }
516
517         if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
518             return
519
520         def convert_version(version):
521             if version.startswith(release.major_version) and version != release.major_version:
522                 return version  # the version number already containt the server version
523             return "%s.%s" % (release.major_version, version)
524
525         def _get_migration_versions(pkg):
526             def __get_dir(tree):
527                 return [d for d in tree if tree[d] is not None]
528
529             versions = list(set(
530                 __get_dir(self.migrations[pkg.name]['module']) +
531                 __get_dir(self.migrations[pkg.name]['maintenance'])
532             ))
533             versions.sort(key=lambda k: parse_version(convert_version(k)))
534             return versions
535
536         def _get_migration_files(pkg, version, stage):
537             """ return a list of tuple (module, file)
538             """
539             m = self.migrations[pkg.name]
540             lst = []
541
542             mapping = {'module': opj(pkg.name, 'migrations'),
543                        'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
544                       }
545
546             for x in mapping.keys():
547                 if version in m[x]:
548                     for f in m[x][version]:
549                         if m[x][version][f] is not None:
550                             continue
551                         if not f.startswith(stage + '-'):
552                             continue
553                         lst.append(opj(mapping[x], version, f))
554             lst.sort()
555             return lst
556
557         def mergedict(a, b):
558             a = a.copy()
559             a.update(b)
560             return a
561
562         from tools.parse_version import parse_version
563
564         parsed_installed_version = parse_version(pkg.installed_version or '')
565         current_version = parse_version(convert_version(pkg.data.get('version', '0')))
566
567         versions = _get_migration_versions(pkg)
568
569         for version in versions:
570             if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
571
572                 strfmt = {'addon': pkg.name,
573                           'stage': stage,
574                           'version': stageformat[stage] % version,
575                           }
576
577                 for pyfile in _get_migration_files(pkg, version, stage):
578                     name, ext = os.path.splitext(os.path.basename(pyfile))
579                     if ext.lower() != '.py':
580                         continue
581                     mod = fp = fp2 = None
582                     try:
583                         fp = tools.file_open(pyfile)
584
585                         # imp.load_source need a real file object, so we create
586                         # one from the file-like object we get from file_open
587                         fp2 = os.tmpfile()
588                         fp2.write(fp.read())
589                         fp2.seek(0)
590                         try:
591                             mod = imp.load_source(name, pyfile, fp2)
592                             logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
593                             mod.migrate(self.cr, pkg.installed_version)
594                         except ImportError:
595                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
596                             raise
597                         except AttributeError:
598                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
599                         except:
600                             raise
601                     finally:
602                         if fp:
603                             fp.close()
604                         if fp2:
605                             fp2.close()
606                         if mod:
607                             del mod
608
609 log = logging.getLogger('init')
610
611 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, **kwargs):
612     """Migrates+Updates or Installs all module nodes from ``graph``
613        :param graph: graph of module nodes to load
614        :param status: status dictionary for keeping track of progress
615        :param perform_checks: whether module descriptors should be checked for validity (prints warnings
616                               for same cases, and even raise osv_except if certificate is invalid)
617        :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
618        :return: list of modules that were installed or updated
619     """
620     def process_sql_file(cr, fp):
621         queries = fp.read().split(';')
622         for query in queries:
623             new_query = ' '.join(query.split())
624             if new_query:
625                 cr.execute(new_query)
626
627     def load_init_update_xml(cr, m, idref, mode, kind):
628         for filename in package.data.get('%s_xml' % kind, []):
629             logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
630             _, ext = os.path.splitext(filename)
631             fp = tools.file_open(opj(m, filename))
632             try:
633                 if ext == '.csv':
634                     noupdate = (kind == 'init')
635                     tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate)
636                 elif ext == '.sql':
637                     process_sql_file(cr, fp)
638                 elif ext == '.yml':
639                     tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs)
640                 else:
641                     tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
642             finally:
643                 fp.close()
644
645     def load_demo_xml(cr, m, idref, mode):
646         for xml in package.data.get('demo_xml', []):
647             name, ext = os.path.splitext(xml)
648             logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
649             fp = tools.file_open(opj(m, xml))
650             try:
651                 if ext == '.csv':
652                     tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
653                 elif ext == '.yml':
654                     tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
655                 else:
656                     tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
657             finally:
658                 fp.close()
659
660     def load_data(cr, module_name, id_map, mode):
661         _load_data(cr, module_name, id_map, mode, 'data')
662
663     def load_demo(cr, module_name, id_map, mode):
664         _load_data(cr, module_name, id_map, mode, 'demo')
665
666     def load_test(cr, module_name, id_map, mode):
667         cr.commit()
668         if not tools.config.options['test_disable']:
669             try:
670                 _load_data(cr, module_name, id_map, mode, 'test')
671             except Exception, e:
672                 logging.getLogger('test').exception('Tests failed to execute in module %s', module_name)
673             finally:
674                 if tools.config.options['test_commit']:
675                     cr.commit()
676                 else:
677                     cr.rollback()
678
679     def _load_data(cr, module_name, id_map, mode, kind):
680         for filename in package.data.get(kind, []):
681             noupdate = (kind == 'demo')
682             _, ext = os.path.splitext(filename)
683             log.info("module %s: loading %s", module_name, filename)
684             pathname = os.path.join(module_name, filename)
685             file = tools.file_open(pathname)
686             try:
687                 if ext == '.sql':
688                     process_sql_file(cr, file)
689                 elif ext == '.csv':
690                     noupdate = (kind == 'init')
691                     tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate)
692                 elif ext == '.yml':
693                     tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate)
694                 else:
695                     tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate)
696             finally:
697                 file.close()
698
699     # **kwargs is passed directly to convert_xml_import
700     if not status:
701         status = {}
702
703     status = status.copy()
704     processed_modules = []
705     statusi = 0
706     pool = pooler.get_pool(cr.dbname)
707     migrations = MigrationManager(cr, graph)
708     modobj = None
709     logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
710
711     for package in graph:
712         if skip_modules and package.name in skip_modules:
713             continue
714         logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
715         migrations.migrate_module(package, 'pre')
716         register_class(package.name)
717         modules = pool.instanciate(package.name, cr)
718         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
719             init_module_objects(cr, package.name, modules)
720         cr.commit()
721
722     for package in graph:
723         status['progress'] = (float(statusi)+0.1) / len(graph)
724         m = package.name
725         mid = package.id
726
727         if skip_modules and m in skip_modules:
728             continue
729
730         if modobj is None:
731             modobj = pool.get('ir.module.module')
732
733         if modobj and perform_checks:
734             modobj.check(cr, 1, [mid])
735
736         idref = {}
737         status['progress'] = (float(statusi)+0.4) / len(graph)
738
739         mode = 'update'
740         if hasattr(package, 'init') or package.state == 'to install':
741             mode = 'init'
742
743         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
744             for kind in ('init', 'update'):
745                 if package.state=='to upgrade':
746                     # upgrading the module information
747                     modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data))
748                 load_init_update_xml(cr, m, idref, mode, kind)
749             load_data(cr, m, idref, mode)
750             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
751                 status['progress'] = (float(statusi)+0.75) / len(graph)
752                 load_demo_xml(cr, m, idref, mode)
753                 load_demo(cr, m, idref, mode)
754                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
755
756                 # launch tests only in demo mode, as most tests will depend
757                 # on demo data. Other tests can be added into the regular
758                 # 'data' section, but should probably not alter the data,
759                 # as there is no rollback.
760                 load_test(cr, m, idref, mode)
761
762             processed_modules.append(package.name)
763
764             migrations.migrate_module(package, 'post')
765
766             if modobj:
767                 ver = release.major_version + '.' + package.data.get('version', '1.0')
768                 # Set new modules and dependencies
769                 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
770                 cr.commit()
771                 # Update translations for all installed languages
772                 modobj.update_translations(cr, 1, [mid], None, {'overwrite': True})
773                 cr.commit()
774
775             package.state = 'installed'
776             for kind in ('init', 'demo', 'update'):
777                 if hasattr(package, kind):
778                     delattr(package, kind)
779
780         statusi += 1
781
782     cr.commit()
783
784     return processed_modules
785
786 def _check_module_names(cr, module_names):
787     mod_names = set(module_names)
788     if 'base' in mod_names:
789         # ignore dummy 'all' module
790         if 'all' in mod_names:
791             mod_names.remove('all')
792     if mod_names:
793         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
794         if cr.dictfetchone()['count'] != len(mod_names):
795             # find out what module name(s) are incorrect:
796             cr.execute("SELECT name FROM ir_module_module")
797             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
798             logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
799
800 def load_modules(db, force_demo=False, status=None, update_module=False):
801     if not status:
802         status = {}
803     cr = db.cursor()
804     if cr:
805         cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'")
806         if len(cr.fetchall())==0:
807             logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
808             tools.init_db(cr)
809             tools.config["init"]["all"] = 1
810             tools.config['update']['all'] = 1
811             if not tools.config['without_demo']:
812                 tools.config["demo"]['all'] = 1
813     force = []
814     if force_demo:
815         force.append('demo')
816
817     # This is a brand new pool, just created in pooler.get_db_and_pool()
818     pool = pooler.get_pool(cr.dbname)
819
820     try:
821         processed_modules = []
822         report = tools.assertion_report()
823         # NOTE: Try to also load the modules that have been marked as uninstallable previously...
824         STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
825         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
826             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
827
828         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
829         graph = create_graph(cr, ['base'], force)
830         if not graph:
831             logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
832             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
833         processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report))
834
835         if tools.config['load_language']:
836             for lang in tools.config['load_language'].split(','):
837                 tools.load_language(cr, lang)
838
839         # STEP 2: Mark other modules to be loaded/updated
840         if update_module:
841             modobj = pool.get('ir.module.module')
842             logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
843             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
844                 modobj.update_list(cr, 1)
845
846             _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
847
848             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
849             if mods:
850                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
851                 if ids:
852                     modobj.button_install(cr, 1, ids)
853
854             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
855             if mods:
856                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
857                 if ids:
858                     modobj.button_upgrade(cr, 1, ids)
859
860             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
861
862             STATES_TO_LOAD += ['to install']
863
864
865         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
866         loop_guardrail = 0
867         while True:
868             loop_guardrail += 1
869             if loop_guardrail > 100:
870                 raise ValueError('Possible recursive module tree detected, aborting.')
871             cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),))
872
873             module_list = [name for (name,) in cr.fetchall() if name not in graph]
874             if not module_list:
875                 break
876
877             new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
878             if new_modules_in_graph == 0:
879                 # nothing to load
880                 break
881
882             logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
883             processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules))
884
885         # load custom models
886         cr.execute('select model from ir_model where state=%s', ('manual',))
887         for model in cr.dictfetchall():
888             pool.get('ir.model').instanciate(cr, 1, model['model'], {})
889
890         # STEP 4: Finish and cleanup
891         if processed_modules:
892             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
893             for (model, name) in cr.fetchall():
894                 model_obj = pool.get(model)
895                 if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
896                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
897
898             # Temporary warning while we remove access rights on osv_memory objects, as they have
899             # been replaced by owner-only access rights
900             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
901             for (model, name) in cr.fetchall():
902                 model_obj = pool.get(model)
903                 if isinstance(model_obj, osv.osv.osv_memory):
904                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
905
906             cr.execute("SELECT model from ir_model")
907             for (model,) in cr.fetchall():
908                 obj = pool.get(model)
909                 if obj:
910                     obj._check_removed_columns(cr, log=True)
911                 else:
912                     logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model)
913
914             # Cleanup orphan records
915             pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
916
917         if report.get_report():
918             logger.notifyChannel('init', netsvc.LOG_INFO, report)
919
920         for kind in ('init', 'demo', 'update'):
921             tools.config[kind] = {}
922
923         cr.commit()
924         if update_module:
925             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
926             for mod_id, mod_name in cr.fetchall():
927                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
928                 for rmod, rid in cr.fetchall():
929                     uid = 1
930                     rmod_module= pool.get(rmod)
931                     if rmod_module:
932                         rmod_module.unlink(cr, uid, [rid])
933                     else:
934                         logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
935                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
936                 cr.commit()
937             #
938             # TODO: remove menu without actions of children
939             #
940             while True:
941                 cr.execute('''delete from
942                         ir_ui_menu
943                     where
944                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
945                     and
946                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
947                     and
948                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
949                 cr.commit()
950                 if not cr.rowcount:
951                     break
952                 else:
953                     logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
954
955             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
956             cr.commit()
957     finally:
958         cr.close()
959
960
961 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: