[FIX] registry: another pass of cleanup for registry signaling
[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             if kind == 'test':
107                 _logger.log(logging.TEST, "module %s: loading %s", module_name, filename)
108             else:
109                 _logger.info("module %s: loading %s", module_name, filename)
110             _, ext = os.path.splitext(filename)
111             pathname = os.path.join(module_name, filename)
112             fp = tools.file_open(pathname)
113             noupdate = False
114             if kind in ('demo', 'demo_xml'):
115                 noupdate = True
116             try:
117                 ext = ext.lower()
118                 if ext == '.csv':
119                     if kind in ('init', 'init_xml'):
120                         noupdate = True
121                     tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate)
122                 elif ext == '.sql':
123                     process_sql_file(cr, fp)
124                 elif ext == '.yml':
125                     tools.convert_yaml_import(cr, module_name, fp, kind, idref, mode, noupdate, report)
126                 elif ext == '.xml':
127                     tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report)
128                 elif ext == '.js':
129                     pass # .js files are valid but ignored here.
130                 else:
131                     _logger.warning("Can't load unknown file type %s.", filename)
132             finally:
133                 fp.close()
134
135     if status is None:
136         status = {}
137
138     processed_modules = []
139     loaded_modules = []
140     pool = pooler.get_pool(cr.dbname)
141     migrations = openerp.modules.migration.MigrationManager(cr, graph)
142     _logger.info('loading %d modules...', len(graph))
143
144     # Query manual fields for all models at once and save them on the registry
145     # so the initialization code for each model does not have to do it
146     # one model at a time.
147     pool.fields_by_model = {}
148     cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',))
149     for field in cr.dictfetchall():
150         pool.fields_by_model.setdefault(field['model'], []).append(field)
151
152     # register, instantiate and initialize models for each modules
153     for index, package in enumerate(graph):
154         module_name = package.name
155         module_id = package.id
156
157         if skip_modules and module_name in skip_modules:
158             continue
159
160         _logger.debug('module %s: loading objects', package.name)
161         migrations.migrate_module(package, 'pre')
162         load_openerp_module(package.name)
163
164         models = pool.load(cr, package)
165
166         loaded_modules.append(package.name)
167         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
168             init_module_models(cr, package.name, models)
169         pool._init_modules.add(package.name)
170         status['progress'] = float(index) / len(graph)
171
172         # Can't put this line out of the loop: ir.module.module will be
173         # registered by init_module_models() above.
174         modobj = pool.get('ir.module.module')
175
176         if perform_checks:
177             modobj.check(cr, SUPERUSER_ID, [module_id])
178
179         idref = {}
180
181         mode = 'update'
182         if hasattr(package, 'init') or package.state == 'to install':
183             mode = 'init'
184
185         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
186             if package.state=='to upgrade':
187                 # upgrading the module information
188                 modobj.write(cr, SUPERUSER_ID, [module_id], modobj.get_values_from_terp(package.data))
189             load_init_xml(module_name, idref, mode)
190             load_update_xml(module_name, idref, mode)
191             load_data(module_name, idref, mode)
192             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
193                 status['progress'] = (index + 0.75) / len(graph)
194                 load_demo_xml(module_name, idref, mode)
195                 load_demo(module_name, idref, mode)
196                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
197
198                 # launch tests only in demo mode, as most tests will depend
199                 # on demo data. Other tests can be added into the regular
200                 # 'data' section, but should probably not alter the data,
201                 # as there is no rollback.
202                 if tools.config.options['test_enable']:
203                     report.record_result(load_test(module_name, idref, mode))
204
205                     # Run the `fast_suite` and `checks` tests given by the module.
206                     if module_name == 'base':
207                         # Also run the core tests after the database is created.
208                         report.record_result(openerp.modules.module.run_unit_tests('openerp'))
209                     report.record_result(openerp.modules.module.run_unit_tests(module_name))
210
211             processed_modules.append(package.name)
212
213             migrations.migrate_module(package, 'post')
214
215             ver = adapt_version(package.data['version'])
216             # Set new modules and dependencies
217             modobj.write(cr, SUPERUSER_ID, [module_id], {'state': 'installed', 'latest_version': ver})
218             # Update translations for all installed languages
219             modobj.update_translations(cr, SUPERUSER_ID, [module_id], None)
220
221             package.state = 'installed'
222             for kind in ('init', 'demo', 'update'):
223                 if hasattr(package, kind):
224                     delattr(package, kind)
225
226         cr.commit()
227
228     # The query won't be valid for models created later (i.e. custom model
229     # created after the registry has been loaded), so empty its result.
230     pool.fields_by_model = None
231     
232     cr.commit()
233
234     return loaded_modules, processed_modules
235
236 def _check_module_names(cr, module_names):
237     mod_names = set(module_names)
238     if 'base' in mod_names:
239         # ignore dummy 'all' module
240         if 'all' in mod_names:
241             mod_names.remove('all')
242     if mod_names:
243         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
244         if cr.dictfetchone()['count'] != len(mod_names):
245             # find out what module name(s) are incorrect:
246             cr.execute("SELECT name FROM ir_module_module")
247             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
248             _logger.warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
249
250 def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules, perform_checks):
251     """Loads modules marked with ``states``, adding them to ``graph`` and
252        ``loaded_modules`` and returns a list of installed/upgraded modules."""
253     processed_modules = []
254     while True:
255         cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(states),))
256         module_list = [name for (name,) in cr.fetchall() if name not in graph]
257         graph.add_modules(cr, module_list, force)
258         _logger.debug('Updating graph with %d more modules', len(module_list))
259         loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules, perform_checks=perform_checks)
260         processed_modules.extend(processed)
261         loaded_modules.extend(loaded)
262         if not processed: break
263     return processed_modules
264
265
266 def load_modules(db, force_demo=False, status=None, update_module=False):
267     # TODO status['progress'] reporting is broken: used twice (and reset each
268     # time to zero) in load_module_graph, not fine-grained enough.
269     # It should be a method exposed by the pool.
270     initialize_sys_path()
271
272     open_openerp_namespace()
273
274     force = []
275     if force_demo:
276         force.append('demo')
277
278     cr = db.cursor()
279     try:
280         if not openerp.modules.db.is_initialized(cr):
281             _logger.info("init db")
282             openerp.modules.db.initialize(cr)
283             tools.config["init"]["all"] = 1
284             tools.config['update']['all'] = 1
285             if not tools.config['without_demo']:
286                 tools.config["demo"]['all'] = 1
287
288         # This is a brand new pool, just created in pooler.get_db_and_pool()
289         pool = pooler.get_pool(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 = pool._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 = pool.get('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
333
334         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
335         # IMPORTANT: this is done in two parts, first loading all installed or
336         #            partially installed modules (i.e. installed/to upgrade), to
337         #            offer a consistent system to the second part: installing
338         #            newly selected modules.
339         #            We include the modules 'to remove' in the first step, because
340         #            they are part of the "currently installed" modules. They will
341         #            be dropped in STEP 6 later, before restarting the loading
342         #            process.
343         states_to_load = ['installed', 'to upgrade', 'to remove']
344         processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules, update_module)
345         processed_modules.extend(processed)
346         if update_module:
347             states_to_load = ['to install']
348             processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules, update_module)
349             processed_modules.extend(processed)
350
351         # load custom models
352         cr.execute('select model from ir_model where state=%s', ('manual',))
353         for model in cr.dictfetchall():
354             pool.get('ir.model').instanciate(cr, SUPERUSER_ID, model['model'], {})
355
356         # STEP 4: Finish and cleanup installations
357         if processed_modules:
358             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
359             for (model, name) in cr.fetchall():
360                 model_obj = pool.get(model)
361                 if model_obj and not model_obj.is_transient():
362                     _logger.warning('The model %s has no access rules, consider adding one. E.g. access_%s,access_%s,model_%s,,1,1,1,1',
363                         model, model.replace('.', '_'), model.replace('.', '_'), model.replace('.', '_'))
364
365             # Temporary warning while we remove access rights on osv_memory objects, as they have
366             # been replaced by owner-only access rights
367             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
368             for (model, name) in cr.fetchall():
369                 model_obj = pool.get(model)
370                 if model_obj and model_obj.is_transient():
371                     _logger.warning('The transient model %s (%s) should not have explicit access rules!', model, name)
372
373             cr.execute("SELECT model from ir_model")
374             for (model,) in cr.fetchall():
375                 obj = pool.get(model)
376                 if obj:
377                     obj._check_removed_columns(cr, log=True)
378                 else:
379                     _logger.warning("Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)", model)
380
381             # Cleanup orphan records
382             pool.get('ir.model.data')._process_end(cr, SUPERUSER_ID, processed_modules)
383
384         for kind in ('init', 'demo', 'update'):
385             tools.config[kind] = {}
386
387         cr.commit()
388
389         # STEP 5: Cleanup menus 
390         # Remove menu items that are not referenced by any of other
391         # (child) menu item, ir_values, or ir_model_data.
392         # TODO: This code could be a method of ir_ui_menu. Remove menu without actions of children
393         if update_module:
394             while True:
395                 cr.execute('''delete from
396                         ir_ui_menu
397                     where
398                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
399                     and
400                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
401                     and
402                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
403                 cr.commit()
404                 if not cr.rowcount:
405                     break
406                 else:
407                     _logger.info('removed %d unused menus', cr.rowcount)
408
409         # STEP 6: Uninstall modules to remove
410         if update_module:
411             # Remove records referenced from ir_model_data for modules to be
412             # removed (and removed the references from ir_model_data).
413             cr.execute("SELECT id FROM ir_module_module WHERE state=%s", ('to remove',))
414             mod_ids_to_remove = [x[0] for x in cr.fetchall()]
415             if mod_ids_to_remove:
416                 pool.get('ir.module.module').module_uninstall(cr, SUPERUSER_ID, mod_ids_to_remove)
417                 # Recursive reload, should only happen once, because there should be no
418                 # modules to remove next time
419                 cr.commit()
420                 _logger.info('Reloading registry once more after uninstalling modules')
421                 return pooler.restart_pool(cr.dbname, force_demo, status, update_module)
422
423         if report.failures:
424             _logger.error('At least one test failed when loading the modules.')
425         else:
426             _logger.info('Modules loaded.')
427
428         # STEP 7: call _register_hook on every model
429         for model in pool.models.values():
430             model._register_hook(cr)
431
432     finally:
433         cr.close()
434
435
436 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: