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