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