[REF] simplified init_db/load_modules:
[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, m, idref, mode):
92         _load_data(cr, m, idref, mode, 'init_xml')
93
94     def load_update_xml(cr, m, idref, mode):
95         _load_data(cr, m, idref, mode, 'update_xml')
96
97     def load_demo_xml(cr, m, idref, mode):
98         _load_data(cr, m, 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     statusi = 0
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, instanciate and initialize models for each modules
161     for package in graph:
162         if skip_modules and package.name in skip_modules:
163             continue
164         logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
165         migrations.migrate_module(package, 'pre')
166         register_module_classes(package.name)
167         models = pool.instanciate(package.name, cr)
168         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
169             init_module_models(cr, package.name, models)
170         cr.commit()
171
172     # load data for each modules
173     modobj = pool.get('ir.module.module')
174     for package in graph:
175         status['progress'] = (float(statusi)+0.1) / len(graph)
176         m = package.name
177         mid = package.id
178
179         if skip_modules and m in skip_modules:
180             continue
181
182         if perform_checks:
183             modobj.check(cr, 1, [mid])
184
185         idref = {}
186         status['progress'] = (float(statusi)+0.4) / len(graph)
187
188         mode = 'update'
189         if hasattr(package, 'init') or package.state == 'to install':
190             mode = 'init'
191
192         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
193             if package.state=='to upgrade':
194                 # upgrading the module information
195                 modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data))
196             load_init_xml(cr, m, idref, mode)
197             load_update_xml(cr, m, idref, mode)
198             load_data(cr, m, idref, mode)
199             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
200                 status['progress'] = (float(statusi)+0.75) / len(graph)
201                 load_demo_xml(cr, m, idref, mode)
202                 load_demo(cr, m, idref, mode)
203                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
204
205                 # launch tests only in demo mode, as most tests will depend
206                 # on demo data. Other tests can be added into the regular
207                 # 'data' section, but should probably not alter the data,
208                 # as there is no rollback.
209                 load_test(cr, m, idref, mode)
210
211             processed_modules.append(package.name)
212
213             migrations.migrate_module(package, 'post')
214
215             ver = release.major_version + '.' + package.data['version']
216             # Set new modules and dependencies
217             modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
218             cr.commit()
219             # Update translations for all installed languages
220             modobj.update_translations(cr, 1, [mid], None)
221             cr.commit()
222
223             package.state = 'installed'
224             for kind in ('init', 'demo', 'update'):
225                 if hasattr(package, kind):
226                     delattr(package, kind)
227
228         statusi += 1
229
230     cr.commit()
231
232     return processed_modules
233
234 def _check_module_names(cr, module_names):
235     mod_names = set(module_names)
236     if 'base' in mod_names:
237         # ignore dummy 'all' module
238         if 'all' in mod_names:
239             mod_names.remove('all')
240     if mod_names:
241         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
242         if cr.dictfetchone()['count'] != len(mod_names):
243             # find out what module name(s) are incorrect:
244             cr.execute("SELECT name FROM ir_module_module")
245             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
246             logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
247
248 def load_modules(db, force_demo=False, status=None, update_module=False):
249     # TODO status['progress'] reporting is broken: used twice (and reset each
250     # time to zero) in load_module_graph, not fine-grained enough.
251     # It should be a method exposed by the pool.
252
253     initialize_sys_path()
254
255     open_openerp_namespace()
256
257     force = []
258     if force_demo:
259         force.append('demo')
260
261     cr = db.cursor()
262     try:
263         if not openerp.modules.db.is_initialized(cr):
264             logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
265             openerp.modules.db.initialize(cr)
266             tools.config["init"]["all"] = 1
267             tools.config['update']['all'] = 1
268             if not tools.config['without_demo']:
269                 tools.config["demo"]['all'] = 1
270
271         # This is a brand new pool, just created in pooler.get_db_and_pool()
272         pool = pooler.get_pool(cr.dbname)
273
274         processed_modules = []
275         report = tools.assertion_report()
276         # NOTE: Try to also load the modules that have been marked as uninstallable previously...
277         STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
278         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
279             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
280
281         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
282         graph = openerp.modules.graph.Graph()
283         graph.add_module(cr, 'base', force)
284         if not graph:
285             logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
286             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
287         processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report))
288
289         if tools.config['load_language']:
290             for lang in tools.config['load_language'].split(','):
291                 tools.load_language(cr, lang)
292
293         # STEP 2: Mark other modules to be loaded/updated
294         if update_module:
295             modobj = pool.get('ir.module.module')
296             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
297                 logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
298                 modobj.update_list(cr, 1)
299
300             _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
301
302             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
303             if mods:
304                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
305                 if ids:
306                     modobj.button_install(cr, 1, ids)
307
308             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
309             if mods:
310                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
311                 if ids:
312                     modobj.button_upgrade(cr, 1, ids)
313
314             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
315
316             STATES_TO_LOAD += ['to install']
317
318
319         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
320         loop_guardrail = 0
321         while True:
322             loop_guardrail += 1
323             if loop_guardrail > 100:
324                 raise ValueError('Possible recursive module tree detected, aborting.')
325             cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),))
326
327             module_list = [name for (name,) in cr.fetchall() if name not in graph]
328             if not module_list:
329                 break
330
331             new_modules_in_graph = graph.add_modules(cr, module_list, force)
332             if new_modules_in_graph == 0:
333                 # nothing to load
334                 break
335
336             logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
337             processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules))
338
339         # load custom models
340         cr.execute('select model from ir_model where state=%s', ('manual',))
341         for model in cr.dictfetchall():
342             pool.get('ir.model').instanciate(cr, 1, model['model'], {})
343
344         # STEP 4: Finish and cleanup
345         if processed_modules:
346             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
347             for (model, name) in cr.fetchall():
348                 model_obj = pool.get(model)
349                 if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
350                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
351
352             # Temporary warning while we remove access rights on osv_memory objects, as they have
353             # been replaced by owner-only access rights
354             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
355             for (model, name) in cr.fetchall():
356                 model_obj = pool.get(model)
357                 if isinstance(model_obj, osv.osv.osv_memory):
358                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
359
360             cr.execute("SELECT model from ir_model")
361             for (model,) in cr.fetchall():
362                 obj = pool.get(model)
363                 if obj:
364                     obj._check_removed_columns(cr, log=True)
365                 else:
366                     logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model)
367
368             # Cleanup orphan records
369             pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
370
371         if report.get_report():
372             logger.notifyChannel('init', netsvc.LOG_INFO, report)
373
374         for kind in ('init', 'demo', 'update'):
375             tools.config[kind] = {}
376
377         cr.commit()
378         if update_module:
379             # Remove records referenced from ir_model_data for modules to be
380             # removed (and removed the references from ir_model_data).
381             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
382             for mod_id, mod_name in cr.fetchall():
383                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
384                 for rmod, rid in cr.fetchall():
385                     uid = 1
386                     rmod_module= pool.get(rmod)
387                     if rmod_module:
388                         # TODO group by module so that we can delete multiple ids in a call
389                         rmod_module.unlink(cr, uid, [rid])
390                     else:
391                         logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
392                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
393                 cr.commit()
394
395             # Remove menu items that are not referenced by any of other
396             # (child) menu item, ir_values, or ir_model_data.
397             # This code could be a method of ir_ui_menu.
398             # TODO: remove menu without actions of children
399             while True:
400                 cr.execute('''delete from
401                         ir_ui_menu
402                     where
403                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
404                     and
405                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
406                     and
407                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
408                 cr.commit()
409                 if not cr.rowcount:
410                     break
411                 else:
412                     logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
413
414             # Pretend that modules to be removed are actually uninstalled.
415             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
416             cr.commit()
417     finally:
418         cr.close()
419
420
421 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: