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