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