[IMP] tests: move tests discovery and execution to openerp.modules.module, removed...
[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-2011 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 base64
28 import imp
29 import itertools
30 import logging
31 import os
32 import re
33 import sys
34 import threading
35 import zipfile
36 import zipimport
37
38 from cStringIO import StringIO
39 from os.path import join as opj
40 from zipfile import PyZipFile, ZIP_DEFLATED
41
42
43 import openerp
44 import openerp.modules.db
45 import openerp.modules.graph
46 import openerp.modules.migration
47 import openerp.netsvc as netsvc
48 import openerp.osv as osv
49 import openerp.pooler as pooler
50 import openerp.release as release
51 import openerp.tools as tools
52 import openerp.tools.osutil as osutil
53
54 from openerp.tools.safe_eval import safe_eval as eval
55 from openerp.tools.translate import _
56 from openerp.modules.module import \
57     get_modules, get_modules_with_version, \
58     load_information_from_description_file, \
59     get_module_resource, zip_directory, \
60     get_module_path, initialize_sys_path, \
61     load_openerp_module, init_module_models
62
63 _logger = logging.getLogger(__name__)
64
65 def open_openerp_namespace():
66     # See comment for open_openerp_namespace.
67     if openerp.conf.deprecation.open_openerp_namespace:
68         for k, v in list(sys.modules.items()):
69             if k.startswith('openerp.') and sys.modules.get(k[8:]) is None:
70                 sys.modules[k[8:]] = v
71
72
73 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None):
74     """Migrates+Updates or Installs all module nodes from ``graph``
75        :param graph: graph of module nodes to load
76        :param status: status dictionary for keeping track of progress
77        :param perform_checks: whether module descriptors should be checked for validity (prints warnings
78                               for same cases, and even raise osv_except if certificate is invalid)
79        :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
80        :return: list of modules that were installed or updated
81     """
82     def process_sql_file(cr, fp):
83         queries = fp.read().split(';')
84         for query in queries:
85             new_query = ' '.join(query.split())
86             if new_query:
87                 cr.execute(new_query)
88
89     load_init_xml = lambda *args: _load_data(cr, *args, kind='init_xml')
90     load_update_xml = lambda *args: _load_data(cr, *args, kind='update_xml')
91     load_demo_xml = lambda *args: _load_data(cr, *args, kind='demo_xml')
92     load_data = lambda *args: _load_data(cr, *args, kind='data')
93     load_demo = lambda *args: _load_data(cr, *args, kind='demo')
94
95     def load_test(module_name, idref, mode):
96         cr.commit()
97         if not tools.config.options['test_disable']:
98             try:
99                 threading.currentThread().testing = True
100                 _load_data(cr, module_name, idref, mode, 'test')
101             except Exception, e:
102                 _logger.exception(
103                     'Tests failed to execute in module %s', module_name)
104             finally:
105                 threading.currentThread().testing = False
106                 if tools.config.options['test_commit']:
107                     cr.commit()
108                 else:
109                     cr.rollback()
110
111     def _load_data(cr, module_name, idref, mode, kind):
112         """
113
114         kind: data, demo, test, init_xml, update_xml, demo_xml.
115
116         noupdate is False, unless it is demo data or it is csv data in
117         init mode.
118
119         """
120         for filename in package.data[kind]:
121             _logger.info("module %s: loading %s", module_name, filename)
122             _, ext = os.path.splitext(filename)
123             pathname = os.path.join(module_name, filename)
124             fp = tools.file_open(pathname)
125             noupdate = False
126             if kind in ('demo', 'demo_xml'):
127                 noupdate = True
128             try:
129                 if ext == '.csv':
130                     if kind in ('init', 'init_xml'):
131                         noupdate = True
132                     tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate)
133                 elif ext == '.sql':
134                     process_sql_file(cr, fp)
135                 elif ext == '.yml':
136                     tools.convert_yaml_import(cr, module_name, fp, idref, mode, noupdate)
137                 else:
138                     tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report)
139             finally:
140                 fp.close()
141
142     if status is None:
143         status = {}
144
145     processed_modules = []
146     loaded_modules = []
147     pool = pooler.get_pool(cr.dbname)
148     migrations = openerp.modules.migration.MigrationManager(cr, graph)
149     _logger.debug('loading %d packages...', len(graph))
150
151     # get db timestamp
152     cr.execute("select now()::timestamp")
153     dt_before_load = cr.fetchone()[0]
154
155     # register, instantiate and initialize models for each modules
156     for index, package in enumerate(graph):
157         module_name = package.name
158         module_id = package.id
159
160         if skip_modules and module_name in skip_modules:
161             continue
162
163         _logger.info('module %s: loading objects', package.name)
164         migrations.migrate_module(package, 'pre')
165         load_openerp_module(package.name)
166
167         models = pool.load(cr, package)
168         loaded_modules.append(package.name)
169         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
170             init_module_models(cr, package.name, models)
171
172         status['progress'] = float(index) / len(graph)
173
174         # Can't put this line out of the loop: ir.module.module will be
175         # registered by init_module_models() above.
176         modobj = pool.get('ir.module.module')
177
178         if perform_checks:
179             modobj.check(cr, 1, [module_id])
180
181         idref = {}
182
183         mode = 'update'
184         if hasattr(package, 'init') or package.state == 'to install':
185             mode = 'init'
186
187         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
188             if package.state=='to upgrade':
189                 # upgrading the module information
190                 modobj.write(cr, 1, [module_id], modobj.get_values_from_terp(package.data))
191             load_init_xml(module_name, idref, mode)
192             load_update_xml(module_name, idref, mode)
193             load_data(module_name, idref, mode)
194             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
195                 status['progress'] = (index + 0.75) / len(graph)
196                 load_demo_xml(module_name, idref, mode)
197                 load_demo(module_name, idref, mode)
198                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
199
200                 # launch tests only in demo mode, as most tests will depend
201                 # on demo data. Other tests can be added into the regular
202                 # 'data' section, but should probably not alter the data,
203                 # as there is no rollback.
204                 load_test(module_name, idref, mode)
205
206                 # Run the `fast_suite` and `checks` tests given by the module.
207                 if module_name == 'base':
208                     # Also run the core tests after the dabase is created.
209                     openerp.modules.module.run_unit_tests('openerp')
210                 openerp.modules.module.run_unit_tests(module_name)
211
212             processed_modules.append(package.name)
213
214             migrations.migrate_module(package, 'post')
215
216             ver = release.major_version + '.' + package.data['version']
217             # Set new modules and dependencies
218             modobj.write(cr, 1, [module_id], {'state': 'installed', 'latest_version': ver})
219             # Update translations for all installed languages
220             modobj.update_translations(cr, 1, [module_id], None)
221
222             package.state = 'installed'
223             for kind in ('init', 'demo', 'update'):
224                 if hasattr(package, kind):
225                     delattr(package, kind)
226
227         cr.commit()
228
229     # mark new res_log records as read
230     cr.execute("update res_log set read=True where create_date >= %s", (dt_before_load,))
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):
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         new_modules_in_graph = 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)
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         report = tools.assertion_report()
292         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
293             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
294
295         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
296         graph = openerp.modules.graph.Graph()
297         graph.add_module(cr, 'base', force)
298         if not graph:
299             _logger.critical('module base cannot be loaded! (hint: verify addons-path)')
300             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
301
302         # processed_modules: for cleanup step after install
303         # loaded_modules: to avoid double loading
304         loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not 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, 1)
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, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
322                 if ids:
323                     modobj.button_install(cr, 1, ids)
324
325             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
326             if mods:
327                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
328                 if ids:
329                     modobj.button_upgrade(cr, 1, 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         states_to_load = ['installed', 'to upgrade']
340         processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules)
341         processed_modules.extend(processed)
342         if update_module:
343             states_to_load = ['to install']
344             processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules)
345             processed_modules.extend(processed)
346
347         # load custom models
348         cr.execute('select model from ir_model where state=%s', ('manual',))
349         for model in cr.dictfetchall():
350             pool.get('ir.model').instanciate(cr, 1, model['model'], {})
351
352         # STEP 4: Finish and cleanup
353         if processed_modules:
354             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
355             for (model, name) in cr.fetchall():
356                 model_obj = pool.get(model)
357                 if model_obj and not model_obj.is_transient():
358                     _logger.warning('Model %s (%s) has no access rules!', model, name)
359
360             # Temporary warning while we remove access rights on osv_memory objects, as they have
361             # been replaced by owner-only access rights
362             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
363             for (model, name) in cr.fetchall():
364                 model_obj = pool.get(model)
365                 if model_obj and model_obj.is_transient():
366                     _logger.warning('The transient model %s (%s) should not have explicit access rules!', model, name)
367
368             cr.execute("SELECT model from ir_model")
369             for (model,) in cr.fetchall():
370                 obj = pool.get(model)
371                 if obj:
372                     obj._check_removed_columns(cr, log=True)
373                 else:
374                     _logger.warning("Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)", model)
375
376             # Cleanup orphan records
377             pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
378
379         for kind in ('init', 'demo', 'update'):
380             tools.config[kind] = {}
381
382         cr.commit()
383         if update_module:
384             # Remove records referenced from ir_model_data for modules to be
385             # removed (and removed the references from ir_model_data).
386             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
387             for mod_id, mod_name in cr.fetchall():
388                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
389                 for rmod, rid in cr.fetchall():
390                     uid = 1
391                     rmod_module= pool.get(rmod)
392                     if rmod_module:
393                         # TODO group by module so that we can delete multiple ids in a call
394                         rmod_module.unlink(cr, uid, [rid])
395                     else:
396                         _logger.error('Could not locate %s to remove res=%d' % (rmod,rid))
397                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
398                 cr.commit()
399
400             # Remove menu items that are not referenced by any of other
401             # (child) menu item, ir_values, or ir_model_data.
402             # This code could be a method of ir_ui_menu.
403             # TODO: remove menu without actions of children
404             while True:
405                 cr.execute('''delete from
406                         ir_ui_menu
407                     where
408                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
409                     and
410                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
411                     and
412                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
413                 cr.commit()
414                 if not cr.rowcount:
415                     break
416                 else:
417                     _logger.info('removed %d unused menus', cr.rowcount)
418
419             # Pretend that modules to be removed are actually uninstalled.
420             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
421             cr.commit()
422
423         _logger.info('Modules loaded.')
424     finally:
425         cr.close()
426
427
428 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: