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