[MERGE] Forward-port saas-5 up to f9bcd67
[odoo/odoo.git] / openerp / modules / loading.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-2014 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 itertools
28 import logging
29 import os
30 import sys
31 import threading
32 import time
33
34 import openerp
35 import openerp.modules.db
36 import openerp.modules.graph
37 import openerp.modules.migration
38 import openerp.modules.registry
39 import openerp.osv as osv
40 import openerp.tools as tools
41 from openerp import SUPERUSER_ID
42
43 from openerp.tools.translate import _
44 from openerp.modules.module import initialize_sys_path, \
45     load_openerp_module, init_module_models, adapt_version
46 from module import runs_post_install
47
48 _logger = logging.getLogger(__name__)
49 _test_logger = logging.getLogger('openerp.tests')
50
51
52 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None):
53     """Migrates+Updates or Installs all module nodes from ``graph``
54        :param graph: graph of module nodes to load
55        :param status: deprecated parameter, unused, left to avoid changing signature in 8.0
56        :param perform_checks: whether module descriptors should be checked for validity (prints warnings
57                               for same cases)
58        :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
59        :return: list of modules that were installed or updated
60     """
61     def load_test(module_name, idref, mode):
62         cr.commit()
63         try:
64             _load_data(cr, module_name, idref, mode, 'test')
65             return True
66         except Exception:
67             _test_logger.exception(
68                 'module %s: an exception occurred in a test', module_name)
69             return False
70         finally:
71             if tools.config.options['test_commit']:
72                 cr.commit()
73             else:
74                 cr.rollback()
75                 # avoid keeping stale xml_id, etc. in cache 
76                 openerp.modules.registry.RegistryManager.clear_caches(cr.dbname)
77
78
79     def _get_files_of_kind(kind):
80         if kind == 'demo':
81             kind = ['demo_xml', 'demo']
82         elif kind == 'data':
83             kind = ['init_xml', 'update_xml', 'data']
84         if isinstance(kind, str):
85             kind = [kind]
86         files = []
87         for k in kind:
88             for f in package.data[k]:
89                 files.append(f)
90                 if k.endswith('_xml') and not (k == 'init_xml' and not f.endswith('.xml')):
91                     # init_xml, update_xml and demo_xml are deprecated except
92                     # for the case of init_xml with yaml, csv and sql files as
93                     # we can't specify noupdate for those file.
94                     correct_key = 'demo' if k.count('demo') else 'data'
95                     _logger.warning(
96                         "module %s: key '%s' is deprecated in favor of '%s' for file '%s'.",
97                         package.name, k, correct_key, f
98                     )
99         return files
100
101     def _load_data(cr, module_name, idref, mode, kind):
102         """
103
104         kind: data, demo, test, init_xml, update_xml, demo_xml.
105
106         noupdate is False, unless it is demo data or it is csv data in
107         init mode.
108
109         """
110         try:
111             if kind in ('demo', 'test'):
112                 threading.currentThread().testing = True
113             for filename in _get_files_of_kind(kind):
114                 _logger.info("loading %s/%s", module_name, filename)
115                 noupdate = False
116                 if kind in ('demo', 'demo_xml') or (filename.endswith('.csv') and kind in ('init', 'init_xml')):
117                     noupdate = True
118                 tools.convert_file(cr, module_name, filename, idref, mode, noupdate, kind, report)
119         finally:
120             if kind in ('demo', 'test'):
121                 threading.currentThread().testing = False
122
123     processed_modules = []
124     loaded_modules = []
125     registry = openerp.registry(cr.dbname)
126     migrations = openerp.modules.migration.MigrationManager(cr, graph)
127     _logger.info('loading %d modules...', len(graph))
128
129     # Query manual fields for all models at once and save them on the registry
130     # so the initialization code for each model does not have to do it
131     # one model at a time.
132     registry.fields_by_model = {}
133     cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',))
134     for field in cr.dictfetchall():
135         registry.fields_by_model.setdefault(field['model'], []).append(field)
136
137     # register, instantiate and initialize models for each modules
138     t0 = time.time()
139     t0_sql = openerp.sql_db.sql_counter
140
141     for index, package in enumerate(graph):
142         module_name = package.name
143         module_id = package.id
144
145         if skip_modules and module_name in skip_modules:
146             continue
147
148         migrations.migrate_module(package, 'pre')
149         load_openerp_module(package.name)
150
151         new_install = package.installed_version is None
152         if new_install:
153             py_module = sys.modules['openerp.addons.%s' % (module_name,)]
154             pre_init = package.info.get('pre_init_hook')
155             if pre_init:
156                 getattr(py_module, pre_init)(cr)
157
158         models = registry.load(cr, package)
159
160         loaded_modules.append(package.name)
161         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
162             registry.setup_models(cr, partial=True)
163             init_module_models(cr, package.name, models)
164
165         # Can't put this line out of the loop: ir.module.module will be
166         # registered by init_module_models() above.
167         modobj = registry['ir.module.module']
168
169         if perform_checks:
170             modobj.check(cr, SUPERUSER_ID, [module_id])
171
172         idref = {}
173
174         mode = 'update'
175         if hasattr(package, 'init') or package.state == 'to install':
176             mode = 'init'
177
178         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
179             if package.state=='to upgrade':
180                 # upgrading the module information
181                 modobj.write(cr, SUPERUSER_ID, [module_id], modobj.get_values_from_terp(package.data))
182             _load_data(cr, module_name, idref, mode, kind='data')
183             has_demo = hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed')
184             if has_demo:
185                 _load_data(cr, module_name, idref, mode, kind='demo')
186                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
187                 modobj.invalidate_cache(cr, SUPERUSER_ID, ['demo'], [module_id])
188
189             migrations.migrate_module(package, 'post')
190
191             if new_install:
192                 post_init = package.info.get('post_init_hook')
193                 if post_init:
194                     getattr(py_module, post_init)(cr, registry)
195
196             registry._init_modules.add(package.name)
197             # validate all the views at a whole
198             registry['ir.ui.view']._validate_module_views(cr, SUPERUSER_ID, module_name)
199
200             if has_demo:
201                 # launch tests only in demo mode, allowing tests to use demo data.
202                 if tools.config.options['test_enable']:
203                     # Yamel test
204                     report.record_result(load_test(module_name, idref, mode))
205                     # Python tests
206                     ir_http = registry['ir.http']
207                     if hasattr(ir_http, '_routing_map'):
208                         # Force routing map to be rebuilt between each module test suite
209                         del(ir_http._routing_map)
210                     report.record_result(openerp.modules.module.run_unit_tests(module_name, cr.dbname))
211
212             processed_modules.append(package.name)
213
214             ver = adapt_version(package.data['version'])
215             # Set new modules and dependencies
216             modobj.write(cr, SUPERUSER_ID, [module_id], {'state': 'installed', 'latest_version': ver})
217             # Update translations for all installed languages
218             modobj.update_translations(cr, SUPERUSER_ID, [module_id], None, {'overwrite': openerp.tools.config["overwrite_existing_translations"]})
219
220             package.state = 'installed'
221             for kind in ('init', 'demo', 'update'):
222                 if hasattr(package, kind):
223                     delattr(package, kind)
224
225         registry._init_modules.add(package.name)
226         cr.commit()
227
228     registry.setup_models(cr, partial=True)
229
230     _logger.log(25, "%s modules loaded in %.2fs, %s queries", len(graph), time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
231
232     # The query won't be valid for models created later (i.e. custom model
233     # created after the registry has been loaded), so empty its result.
234     registry.fields_by_model = None
235
236     cr.commit()
237
238     return loaded_modules, processed_modules
239
240 def _check_module_names(cr, module_names):
241     mod_names = set(module_names)
242     if 'base' in mod_names:
243         # ignore dummy 'all' module
244         if 'all' in mod_names:
245             mod_names.remove('all')
246     if mod_names:
247         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
248         if cr.dictfetchone()['count'] != len(mod_names):
249             # find out what module name(s) are incorrect:
250             cr.execute("SELECT name FROM ir_module_module")
251             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
252             _logger.warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
253
254 def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules, perform_checks):
255     """Loads modules marked with ``states``, adding them to ``graph`` and
256        ``loaded_modules`` and returns a list of installed/upgraded modules."""
257     processed_modules = []
258     while True:
259         cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(states),))
260         module_list = [name for (name,) in cr.fetchall() if name not in graph]
261         graph.add_modules(cr, module_list, force)
262         _logger.debug('Updating graph with %d more modules', len(module_list))
263         loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules, perform_checks=perform_checks)
264         processed_modules.extend(processed)
265         loaded_modules.extend(loaded)
266         if not processed: break
267     return processed_modules
268
269 def load_modules(db, force_demo=False, status=None, update_module=False):
270     initialize_sys_path()
271
272     force = []
273     if force_demo:
274         force.append('demo')
275
276     cr = db.cursor()
277     try:
278         if not openerp.modules.db.is_initialized(cr):
279             _logger.info("init db")
280             openerp.modules.db.initialize(cr)
281             update_module = True # process auto-installed modules
282             tools.config["init"]["all"] = 1
283             tools.config['update']['all'] = 1
284             if not tools.config['without_demo']:
285                 tools.config["demo"]['all'] = 1
286
287         # This is a brand new registry, just created in
288         # openerp.modules.registry.RegistryManager.new().
289         registry = openerp.registry(cr.dbname)
290
291         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
292             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
293
294         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
295         graph = openerp.modules.graph.Graph()
296         graph.add_module(cr, 'base', force)
297         if not graph:
298             _logger.critical('module base cannot be loaded! (hint: verify addons-path)')
299             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
300
301         # processed_modules: for cleanup step after install
302         # loaded_modules: to avoid double loading
303         report = registry._assertion_report
304         loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=update_module, report=report)
305
306         if tools.config['load_language']:
307             for lang in tools.config['load_language'].split(','):
308                 tools.load_language(cr, lang)
309
310         # STEP 2: Mark other modules to be loaded/updated
311         if update_module:
312             modobj = registry['ir.module.module']
313             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
314                 _logger.info('updating modules list')
315                 modobj.update_list(cr, SUPERUSER_ID)
316
317             _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
318
319             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
320             if mods:
321                 ids = modobj.search(cr, SUPERUSER_ID, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
322                 if ids:
323                     modobj.button_install(cr, SUPERUSER_ID, ids)
324
325             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
326             if mods:
327                 ids = modobj.search(cr, SUPERUSER_ID, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
328                 if ids:
329                     modobj.button_upgrade(cr, SUPERUSER_ID, ids)
330
331             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
332             modobj.invalidate_cache(cr, SUPERUSER_ID, ['state'])
333
334
335         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
336         # IMPORTANT: this is done in two parts, first loading all installed or
337         #            partially installed modules (i.e. installed/to upgrade), to
338         #            offer a consistent system to the second part: installing
339         #            newly selected modules.
340         #            We include the modules 'to remove' in the first step, because
341         #            they are part of the "currently installed" modules. They will
342         #            be dropped in STEP 6 later, before restarting the loading
343         #            process.
344         # IMPORTANT 2: We have to loop here until all relevant modules have been
345         #              processed, because in some rare cases the dependencies have
346         #              changed, and modules that depend on an uninstalled module
347         #              will not be processed on the first pass.
348         #              It's especially useful for migrations.
349         previously_processed = -1
350         while previously_processed < len(processed_modules):
351             previously_processed = len(processed_modules)
352             processed_modules += load_marked_modules(cr, graph,
353                 ['installed', 'to upgrade', 'to remove'],
354                 force, status, report, loaded_modules, update_module)
355             if update_module:
356                 processed_modules += load_marked_modules(cr, graph,
357                     ['to install'], force, status, report,
358                     loaded_modules, update_module)
359
360         # load custom models
361         cr.execute('select model from ir_model where state=%s', ('manual',))
362         for model in cr.dictfetchall():
363             registry['ir.model'].instanciate(cr, SUPERUSER_ID, model['model'], {})
364         registry.setup_models(cr)
365
366         # STEP 4: Finish and cleanup installations
367         if processed_modules:
368             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
369             for (model, name) in cr.fetchall():
370                 if model in registry and not registry[model].is_transient() and not isinstance(registry[model], openerp.osv.orm.AbstractModel):
371                     _logger.warning('The model %s has no access rules, consider adding one. E.g. access_%s,access_%s,model_%s,,1,1,1,1',
372                         model, model.replace('.', '_'), model.replace('.', '_'), model.replace('.', '_'))
373
374             # Temporary warning while we remove access rights on osv_memory objects, as they have
375             # been replaced by owner-only access rights
376             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
377             for (model, name) in cr.fetchall():
378                 if model in registry and registry[model].is_transient():
379                     _logger.warning('The transient model %s (%s) should not have explicit access rules!', model, name)
380
381             cr.execute("SELECT model from ir_model")
382             for (model,) in cr.fetchall():
383                 if model in registry:
384                     registry[model]._check_removed_columns(cr, log=True)
385                 else:
386                     _logger.warning("Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)", model)
387
388             # Cleanup orphan records
389             registry['ir.model.data']._process_end(cr, SUPERUSER_ID, processed_modules)
390
391         for kind in ('init', 'demo', 'update'):
392             tools.config[kind] = {}
393
394         cr.commit()
395
396         # STEP 5: Cleanup menus 
397         # Remove menu items that are not referenced by any of other
398         # (child) menu item, ir_values, or ir_model_data.
399         # TODO: This code could be a method of ir_ui_menu. Remove menu without actions of children
400         if update_module:
401             while True:
402                 cr.execute('''delete from
403                         ir_ui_menu
404                     where
405                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
406                     and
407                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
408                     and
409                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
410                 cr.commit()
411                 if not cr.rowcount:
412                     break
413                 else:
414                     _logger.info('removed %d unused menus', cr.rowcount)
415
416         # STEP 6: Uninstall modules to remove
417         if update_module:
418             # Remove records referenced from ir_model_data for modules to be
419             # removed (and removed the references from ir_model_data).
420             cr.execute("SELECT name, id FROM ir_module_module WHERE state=%s", ('to remove',))
421             modules_to_remove = dict(cr.fetchall())
422             if modules_to_remove:
423                 pkgs = reversed([p for p in graph if p.name in modules_to_remove])
424                 for pkg in pkgs:
425                     uninstall_hook = pkg.info.get('uninstall_hook')
426                     if uninstall_hook:
427                         py_module = sys.modules['openerp.addons.%s' % (pkg.name,)]
428                         getattr(py_module, uninstall_hook)(cr, registry)
429
430                 registry['ir.module.module'].module_uninstall(cr, SUPERUSER_ID, modules_to_remove.values())
431                 # Recursive reload, should only happen once, because there should be no
432                 # modules to remove next time
433                 cr.commit()
434                 _logger.info('Reloading registry once more after uninstalling modules')
435                 return openerp.modules.registry.RegistryManager.new(cr.dbname, force_demo, status, update_module)
436
437         # STEP 7: verify custom views on every model
438         if update_module:
439             Views = registry['ir.ui.view']
440             custom_view_test = True
441             for model in registry.models.keys():
442                 if not Views._validate_custom_views(cr, SUPERUSER_ID, model):
443                     custom_view_test = False
444                     _logger.error('invalid custom view(s) for model %s', model)
445             report.record_result(custom_view_test)
446
447         if report.failures:
448             _logger.error('At least one test failed when loading the modules.')
449         else:
450             _logger.info('Modules loaded.')
451
452         # STEP 8: call _register_hook on every model
453         for model in registry.models.values():
454             model._register_hook(cr)
455
456         # STEP 9: Run the post-install tests
457         cr.commit()
458
459         t0 = time.time()
460         t0_sql = openerp.sql_db.sql_counter
461         if openerp.tools.config['test_enable']:
462             cr.execute("SELECT name FROM ir_module_module WHERE state='installed'")
463             for module_name in cr.fetchall():
464                 report.record_result(openerp.modules.module.run_unit_tests(module_name[0], cr.dbname, position=runs_post_install))
465             _logger.log(25, "All post-tested in %.2fs, %s queries", time.time() - t0, openerp.sql_db.sql_counter - t0_sql)
466     finally:
467         cr.close()
468
469 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: