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