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