[MERGE] merged trunk.
[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     register_module_classes, init_module_models
62
63 logger = netsvc.Logger()
64
65
66 def open_openerp_namespace():
67     # See comment for open_openerp_namespace.
68     if openerp.conf.deprecation.open_openerp_namespace:
69         for k, v in list(sys.modules.items()):
70             if k.startswith('openerp.') and sys.modules.get(k[8:]) is None:
71                 sys.modules[k[8:]] = v
72
73
74 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None):
75     """Migrates+Updates or Installs all module nodes from ``graph``
76        :param graph: graph of module nodes to load
77        :param status: status dictionary for keeping track of progress
78        :param perform_checks: whether module descriptors should be checked for validity (prints warnings
79                               for same cases, and even raise osv_except if certificate is invalid)
80        :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
81        :return: list of modules that were installed or updated
82     """
83     logger = logging.getLogger('init.load')
84     def process_sql_file(cr, fp):
85         queries = fp.read().split(';')
86         for query in queries:
87             new_query = ' '.join(query.split())
88             if new_query:
89                 cr.execute(new_query)
90
91     load_init_xml = lambda *args: _load_data(cr, *args, kind='init_xml')
92     load_update_xml = lambda *args: _load_data(cr, *args, kind='update_xml')
93     load_demo_xml = lambda *args: _load_data(cr, *args, kind='demo_xml')
94     load_data = lambda *args: _load_data(cr, *args, kind='data')
95     load_demo = lambda *args: _load_data(cr, *args, kind='demo')
96
97     def load_test(module_name, idref, mode):
98         cr.commit()
99         if not tools.config.options['test_disable']:
100             try:
101                 threading.currentThread().testing = True
102                 _load_data(cr, module_name, idref, mode, 'test')
103             except Exception, e:
104                 logging.getLogger('init.test').exception(
105                     'Tests failed to execute in module %s', module_name)
106             finally:
107                 threading.currentThread().testing = False
108                 if tools.config.options['test_commit']:
109                     cr.commit()
110                 else:
111                     cr.rollback()
112
113     def _load_data(cr, module_name, idref, mode, kind):
114         """
115
116         kind: data, demo, test, init_xml, update_xml, demo_xml.
117
118         noupdate is False, unless it is demo data or it is csv data in
119         init mode.
120
121         """
122         for filename in package.data[kind]:
123             logger.info("module %s: loading %s", module_name, filename)
124             _, ext = os.path.splitext(filename)
125             pathname = os.path.join(module_name, filename)
126             fp = tools.file_open(pathname)
127             noupdate = False
128             if kind in ('demo', 'demo_xml'):
129                 noupdate = True
130             try:
131                 if ext == '.csv':
132                     if kind in ('init', 'init_xml'):
133                         noupdate = True
134                     tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate)
135                 elif ext == '.sql':
136                     process_sql_file(cr, fp)
137                 elif ext == '.yml':
138                     tools.convert_yaml_import(cr, module_name, fp, idref, mode, noupdate)
139                 else:
140                     tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report)
141             finally:
142                 fp.close()
143
144     if status is None:
145         status = {}
146
147     processed_modules = []
148     loaded_modules = []
149     pool = pooler.get_pool(cr.dbname)
150     migrations = openerp.modules.migration.MigrationManager(cr, graph)
151     logger.debug('loading %d packages...', len(graph))
152
153     # get db timestamp
154     cr.execute("select now()::timestamp")
155     dt_before_load = cr.fetchone()[0]
156
157     # register, instantiate and initialize models for each modules
158     for index, package in enumerate(graph):
159         module_name = package.name
160         module_id = package.id
161
162         if skip_modules and module_name in skip_modules:
163             continue
164
165         logger.info('module %s: loading objects', package.name)
166         migrations.migrate_module(package, 'pre')
167         register_module_classes(package.name)
168         models = pool.load(cr, package)
169         loaded_modules.append(package.name)
170         if package.state in ('to install', 'to upgrade'):
171             init_module_models(cr, package.name, models)
172
173         status['progress'] = float(index) / len(graph)
174
175         # Can't put this line out of the loop: ir.module.module will be
176         # registered by init_module_models() above.
177         modobj = pool.get('ir.module.module')
178
179         if perform_checks:
180             modobj.check(cr, 1, [module_id])
181
182         idref = {}
183
184         if package.state == 'to install':
185             mode = 'init'
186         else:
187             mode = 'update'
188
189         if package.state in ('to install', 'to upgrade'):
190             if package.state=='to upgrade':
191                 # upgrading the module information
192                 modobj.write(cr, 1, [module_id], modobj.get_values_from_terp(package.data))
193             load_init_xml(module_name, idref, mode)
194             load_update_xml(module_name, idref, mode)
195             load_data(module_name, idref, mode)
196             if package.dbdemo and package.state != 'installed':
197                 status['progress'] = (index + 0.75) / len(graph)
198                 load_demo_xml(module_name, idref, mode)
199                 load_demo(module_name, idref, mode)
200                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
201
202                 # launch tests only in demo mode, as most tests will depend
203                 # on demo data. Other tests can be added into the regular
204                 # 'data' section, but should probably not alter the data,
205                 # as there is no rollback.
206                 load_test(module_name, idref, mode)
207
208             processed_modules.append(package.name)
209
210             migrations.migrate_module(package, 'post')
211
212             ver = release.major_version + '.' + package.data['version']
213             # Set new modules and dependencies
214             modobj.write(cr, 1, [module_id], {'state': 'installed', 'latest_version': ver})
215             # Update translations for all installed languages
216             modobj.update_translations(cr, 1, [module_id], None)
217
218             package.state = 'installed'
219
220         cr.commit()
221
222     # mark new res_log records as read
223     cr.execute("update res_log set read=True where create_date >= %s", (dt_before_load,))
224
225     cr.commit()
226
227     return loaded_modules, processed_modules
228
229 def _check_module_names(cr, module_names):
230     mod_names = set(module_names)
231     if 'base' in mod_names:
232         # ignore dummy 'all' module
233         if 'all' in mod_names:
234             mod_names.remove('all')
235     if mod_names:
236         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
237         if cr.dictfetchone()['count'] != len(mod_names):
238             # find out what module name(s) are incorrect:
239             cr.execute("SELECT name FROM ir_module_module")
240             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
241             logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
242
243 def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules):
244     """Loads modules marked with ``states``, adding them to ``graph`` and
245        ``loaded_modules`` and returns a list of installed/upgraded modules."""
246     processed_modules = []
247     while True:
248         cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(states),))
249         module_list = [name for (name,) in cr.fetchall() if name not in graph]
250         new_modules_in_graph = graph.add_modules(cr, module_list, force)
251         logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
252         loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules)
253         processed_modules.extend(processed)
254         loaded_modules.extend(loaded)
255         if not processed: break
256     return processed_modules
257
258
259 def load_modules(db, force_demo=False, status=None, update_module=False):
260     # TODO status['progress'] reporting is broken: used twice (and reset each
261     # time to zero) in load_module_graph, not fine-grained enough.
262     # It should be a method exposed by the pool.
263     initialize_sys_path()
264
265     open_openerp_namespace()
266
267     force = []
268     if force_demo:
269         force.append('demo')
270
271     cr = db.cursor()
272     try:
273         if not openerp.modules.db.is_initialized(cr):
274             logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
275             openerp.modules.db.initialize(cr)
276             update_module = True
277
278         # This is a brand new pool, just created in pooler.get_db_and_pool()
279         pool = pooler.get_pool(cr.dbname)
280
281         report = tools.assertion_report()
282         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
283             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
284
285         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
286         graph = openerp.modules.graph.Graph()
287         graph.add_module(cr, 'base', force_demo)
288         if not graph:
289             logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
290             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
291
292         # processed_modules: for cleanup step after install
293         # loaded_modules: to avoid double loading
294         # After load_module_graph(), 'base' has been installed or updated and its state is 'installed'.
295         loaded_modules, processed_modules = load_module_graph(cr, graph, status, report=report)
296
297         if tools.config['load_language']:
298             for lang in tools.config['load_language'].split(','):
299                 tools.load_language(cr, lang)
300
301         # STEP 2: Mark other modules to be loaded/updated
302         # This is a one-shot use of tools.config[init|update] from the command line
303         # arguments. It is directly cleared to not interfer with later create/update
304         # issued via RPC.
305         if update_module:
306             modobj = pool.get('ir.module.module')
307             if ('base' in tools.config['init']) or ('base' in tools.config['update']) \
308                 or ('all' in tools.config['init']) or ('all' in tools.config['update']):
309                 logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
310                 modobj.update_list(cr, 1)
311
312             if 'all' in tools.config['init']:
313                 ids = modobj.search(cr, 1, [])
314                 tools.config['init'] = dict.fromkeys([m['name'] for m in modobj.read(cr, 1, ids, ['name'])], 1)
315
316             _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
317
318             mods = [k for k in tools.config['init'] if tools.config['init'][k] and k not in ('base', 'all')]
319             ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
320             if ids:
321                 modobj.button_install(cr, 1, ids) # goes from 'uninstalled' to 'to install'
322
323             mods = [k for k in tools.config['update'] if tools.config['update'][k] and k not in ('base', 'all')]
324             ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
325             if ids:
326                 modobj.button_upgrade(cr, 1, ids) # goes from 'installed' to 'to upgrade'
327
328         for kind in ('init', 'demo', 'update'):
329             tools.config[kind] = {}
330
331         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
332         # IMPORTANT: this is done in two parts, first loading all installed or
333         #            partially installed modules (i.e. installed/to upgrade), to
334         #            offer a consistent system to the second part: installing
335         #            newly selected modules.
336         states_to_load = ['installed', 'to upgrade']
337         processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules)
338         processed_modules.extend(processed)
339         if update_module:
340             states_to_load = ['to install']
341             processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules)
342             processed_modules.extend(processed)
343
344         # load custom models
345         cr.execute('select model from ir_model where state=%s', ('manual',))
346         for model in cr.dictfetchall():
347             pool.get('ir.model').instanciate(cr, 1, model['model'], {})
348
349         # STEP 4: Finish and cleanup
350         if processed_modules:
351             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
352             for (model, name) in cr.fetchall():
353                 model_obj = pool.get(model)
354                 if model_obj and not model_obj.is_transient():
355                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'Model %s (%s) has no access rules!' % (model, name))
356
357             # Temporary warning while we remove access rights on osv_memory objects, as they have
358             # been replaced by owner-only access rights
359             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
360             for (model, name) in cr.fetchall():
361                 model_obj = pool.get(model)
362                 if model_obj and model_obj.is_transient():
363                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'The transient model %s (%s) should not have explicit access rules!' % (model, name))
364
365             cr.execute("SELECT model from ir_model")
366             for (model,) in cr.fetchall():
367                 obj = pool.get(model)
368                 if obj:
369                     obj._check_removed_columns(cr, log=True)
370                 else:
371                     logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)" % model)
372
373             # Cleanup orphan records
374             pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
375
376         if report.get_report():
377             logger.notifyChannel('init', netsvc.LOG_INFO, report)
378
379         cr.commit()
380         if update_module:
381             # Remove records referenced from ir_model_data for modules to be
382             # removed (and removed the references from ir_model_data).
383             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
384             for mod_id, mod_name in cr.fetchall():
385                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
386                 for rmod, rid in cr.fetchall():
387                     uid = 1
388                     rmod_module= pool.get(rmod)
389                     if rmod_module:
390                         # TODO group by module so that we can delete multiple ids in a call
391                         rmod_module.unlink(cr, uid, [rid])
392                     else:
393                         logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
394                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
395                 cr.commit()
396
397             # Remove menu items that are not referenced by any of other
398             # (child) menu item, ir_values, or ir_model_data.
399             # This code could be a method of ir_ui_menu.
400             # TODO: remove menu without actions of children
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.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
415
416             # Pretend that modules to be removed are actually uninstalled.
417             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
418             cr.commit()
419     finally:
420         cr.close()
421
422
423 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: