[IMP] website theme: update themes to fix CSS glitches, add Yeti theme, fix Default...
[odoo/odoo.git] / openerp / modules / module.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-2012 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 import imp
24 import itertools
25 import os
26 from os.path import join as opj
27 import sys
28 import types
29 import zipimport
30
31 import openerp.tools as tools
32 import openerp.tools.osutil as osutil
33 from openerp.tools.safe_eval import safe_eval as eval
34
35 import zipfile
36 import openerp.release as release
37
38 import re
39 import base64
40 from zipfile import PyZipFile, ZIP_DEFLATED
41 from cStringIO import StringIO
42
43 import logging
44
45 _logger = logging.getLogger(__name__)
46 _test_logger = logging.getLogger('openerp.tests')
47
48 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
49 ad_paths = []
50
51 # Modules already loaded
52 loaded = []
53
54 _logger = logging.getLogger(__name__)
55
56 class AddonsImportHook(object):
57     """
58     Import hook to load OpenERP addons from multiple paths.
59
60     OpenERP implements its own import-hook to load its addons. OpenERP
61     addons are Python modules. Originally, they were each living in their
62     own top-level namespace, e.g. the sale module, or the hr module. For
63     backward compatibility, `import <module>` is still supported. Now they
64     are living in `openerp.addons`. The good way to import such modules is
65     thus `import openerp.addons.module`.
66     """
67
68     def find_module(self, module_name, package_path):
69         module_parts = module_name.split('.')
70         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
71             return self # We act as a loader too.
72
73     def load_module(self, module_name):
74
75         module_parts = module_name.split('.')
76         if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
77             module_part = module_parts[2]
78             if module_name in sys.modules:
79                 return sys.modules[module_name]
80
81         # Note: we don't support circular import.
82         f, path, descr = imp.find_module(module_part, ad_paths)
83         mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
84         sys.modules['openerp.addons.' + module_part] = mod
85         return mod
86
87 def initialize_sys_path():
88     """
89     Setup an import-hook to be able to import OpenERP addons from the different
90     addons paths.
91
92     This ensures something like ``import crm`` (or even
93     ``import openerp.addons.crm``) works even if the addons are not in the
94     PYTHONPATH.
95     """
96     global ad_paths
97     if ad_paths:
98         return
99
100     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
101     ad_paths.append(os.path.abspath(_ad)) # for get_module_path
102     sys.meta_path.append(AddonsImportHook())
103
104 def get_module_path(module, downloaded=False, display_warning=True):
105     """Return the path of the given module.
106
107     Search the addons paths and return the first path where the given
108     module is found. If downloaded is True, return the default addons
109     path if nothing else is found.
110
111     """
112     initialize_sys_path()
113     for adp in ad_paths:
114         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
115             return opj(adp, module)
116
117     if downloaded:
118         return opj(_ad, module)
119     if display_warning:
120         _logger.warning('module %s: module not found', module)
121     return False
122
123
124 def get_module_filetree(module, dir='.'):
125     path = get_module_path(module)
126     if not path:
127         return False
128
129     dir = os.path.normpath(dir)
130     if dir == '.':
131         dir = ''
132     if dir.startswith('..') or (dir and dir[0] == '/'):
133         raise Exception('Cannot access file outside the module')
134
135     if not os.path.isdir(path):
136         # zipmodule
137         zip = zipfile.ZipFile(path + ".zip")
138         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
139     else:
140         files = osutil.listdir(path, True)
141
142     tree = {}
143     for f in files:
144         if not f.startswith(dir):
145             continue
146
147         if dir:
148             f = f[len(dir)+int(not dir.endswith('/')):]
149         lst = f.split(os.sep)
150         current = tree
151         while len(lst) != 1:
152             current = current.setdefault(lst.pop(0), {})
153         current[lst.pop(0)] = None
154
155     return tree
156
157 def zip_directory(directory, b64enc=True, src=True):
158     """Compress a directory
159
160     @param directory: The directory to compress
161     @param base64enc: if True the function will encode the zip file with base64
162     @param src: Integrate the source files
163
164     @return: a string containing the zip file
165     """
166
167     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
168
169     def _zippy(archive, path, src=True):
170         path = os.path.abspath(path)
171         base = os.path.basename(path)
172         for f in osutil.listdir(path, True):
173             bf = os.path.basename(f)
174             if not RE_exclude.search(bf) and (src or bf == '__openerp__.py' or not bf.endswith('.py')):
175                 archive.write(os.path.join(path, f), os.path.join(base, f))
176
177     archname = StringIO()
178     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
179
180     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
181     directory = tools.ustr(directory).encode('utf-8')
182
183     archive.writepy(directory)
184     _zippy(archive, directory, src=src)
185     archive.close()
186     archive_data = archname.getvalue()
187     archname.close()
188
189     if b64enc:
190         return base64.encodestring(archive_data)
191
192     return archive_data
193
194 def get_module_as_zip(modulename, b64enc=True, src=True):
195     """Generate a module as zip file with the source or not and can do a base64 encoding
196
197     @param modulename: The module name
198     @param b64enc: if True the function will encode the zip file with base64
199     @param src: Integrate the source files
200
201     @return: a stream to store in a file-like object
202     """
203
204     ap = get_module_path(str(modulename))
205     if not ap:
206         raise Exception('Unable to find path for module %s' % modulename)
207
208     ap = ap.encode('utf8')
209     if os.path.isfile(ap + '.zip'):
210         val = file(ap + '.zip', 'rb').read()
211         if b64enc:
212             val = base64.encodestring(val)
213     else:
214         val = zip_directory(ap, b64enc, src)
215
216     return val
217
218
219 def get_module_resource(module, *args):
220     """Return the full path of a resource of the given module.
221
222     :param module: module name
223     :param list(str) args: resource path components within module
224
225     :rtype: str
226     :return: absolute path to the resource
227
228     TODO name it get_resource_path
229     TODO make it available inside on osv object (self.get_resource_path)
230     """
231     mod_path = get_module_path(module)
232     if not mod_path: return False
233     resource_path = opj(mod_path, *args)
234     if os.path.isdir(mod_path):
235         # the module is a directory - ignore zip behavior
236         if os.path.exists(resource_path):
237             return resource_path
238     elif zipfile.is_zipfile(mod_path + '.zip'):
239         zip = zipfile.ZipFile( mod_path + ".zip")
240         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
241         resource_path = '/'.join(args)
242         if resource_path in files:
243             return opj(mod_path, resource_path)
244     return False
245
246 def get_module_icon(module):
247     iconpath = ['static', 'src', 'img', 'icon.png']
248     if get_module_resource(module, *iconpath):
249         return ('/' + module + '/') + '/'.join(iconpath)
250     return '/base/'  + '/'.join(iconpath)
251
252 def load_information_from_description_file(module):
253     """
254     :param module: The name of the module (sale, purchase, ...)
255     """
256
257     terp_file = get_module_resource(module, '__openerp__.py')
258     mod_path = get_module_path(module)
259     if terp_file:
260         info = {}
261         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
262             # default values for descriptor
263             info = {
264                 'application': False,
265                 'author': '',
266                 'auto_install': False,
267                 'category': 'Uncategorized',
268                 'depends': [],
269                 'description': '',
270                 'icon': get_module_icon(module),
271                 'installable': True,
272                 'license': 'AGPL-3',
273                 'name': False,
274                 'post_load': None,
275                 'version': '1.0',
276                 'web': False,
277                 'website': '',
278                 'sequence': 100,
279                 'summary': '',
280             }
281             info.update(itertools.izip(
282                 'depends data demo test init_xml update_xml demo_xml'.split(),
283                 iter(list, None)))
284
285             f = tools.file_open(terp_file)
286             try:
287                 info.update(eval(f.read()))
288             finally:
289                 f.close()
290
291             if 'active' in info:
292                 # 'active' has been renamed 'auto_install'
293                 info['auto_install'] = info['active']
294
295             info['version'] = adapt_version(info['version'])
296             return info
297
298     #TODO: refactor the logger in this file to follow the logging guidelines
299     #      for 6.0
300     _logger.debug('module %s: no __openerp__.py file found.', module)
301     return {}
302
303
304 def init_module_models(cr, module_name, obj_list):
305     """ Initialize a list of models.
306
307     Call _auto_init and init on each model to create or update the
308     database tables supporting the models.
309
310     TODO better explanation of _auto_init and init.
311
312     """
313     _logger.info('module %s: creating or updating database tables', module_name)
314     todo = []
315     for obj in obj_list:
316         result = obj._auto_init(cr, {'module': module_name})
317         if result:
318             todo += result
319         if hasattr(obj, 'init'):
320             obj.init(cr)
321         cr.commit()
322     for obj in obj_list:
323         obj._auto_end(cr, {'module': module_name})
324         cr.commit()
325     todo.sort()
326     for t in todo:
327         t[1](cr, *t[2])
328     cr.commit()
329
330 def load_openerp_module(module_name):
331     """ Load an OpenERP module, if not already loaded.
332
333     This loads the module and register all of its models, thanks to either
334     the MetaModel metaclass, or the explicit instantiation of the model.
335     This is also used to load server-wide module (i.e. it is also used
336     when there is no model to register).
337     """
338     global loaded
339     if module_name in loaded:
340         return
341
342     initialize_sys_path()
343     try:
344         mod_path = get_module_path(module_name)
345         zip_mod_path = '' if not mod_path else mod_path + '.zip'
346         if not os.path.isfile(zip_mod_path):
347             __import__('openerp.addons.' + module_name)
348         else:
349             zimp = zipimport.zipimporter(zip_mod_path)
350             zimp.load_module(module_name)
351
352         # Call the module's post-load hook. This can done before any model or
353         # data has been initialized. This is ok as the post-load hook is for
354         # server-wide (instead of registry-specific) functionalities.
355         info = load_information_from_description_file(module_name)
356         if info['post_load']:
357             getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
358
359     except Exception, e:
360         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
361         msg = "Couldn't load %smodule %s" % (mt, module_name)
362         _logger.critical(msg)
363         _logger.critical(e)
364         raise
365     else:
366         loaded.append(module_name)
367
368 def get_modules():
369     """Returns the list of module names
370     """
371     def listdir(dir):
372         def clean(name):
373             name = os.path.basename(name)
374             if name[-4:] == '.zip':
375                 name = name[:-4]
376             return name
377
378         def is_really_module(name):
379             manifest_name = opj(dir, name, '__openerp__.py')
380             zipfile_name = opj(dir, name)
381             return os.path.isfile(manifest_name) or zipfile.is_zipfile(zipfile_name)
382         return map(clean, filter(is_really_module, os.listdir(dir)))
383
384     plist = []
385     initialize_sys_path()
386     for ad in ad_paths:
387         plist.extend(listdir(ad))
388     return list(set(plist))
389
390
391 def get_modules_with_version():
392     modules = get_modules()
393     res = dict.fromkeys(modules, adapt_version('1.0'))
394     for module in modules:
395         try:
396             info = load_information_from_description_file(module)
397             res[module] = info['version']
398         except Exception:
399             continue
400     return res
401
402 def adapt_version(version):
403     serie = release.major_version
404     if version == serie or not version.startswith(serie + '.'):
405         version = '%s.%s' % (serie, version)
406     return version
407
408
409 def get_test_modules(module, submodule, explode):
410     """
411     Return a list of submodules containing tests.
412     `submodule` can be:
413       - None
414       - the name of a submodule
415       - '__fast_suite__'
416       - '__sanity_checks__'
417     """
418     # Turn command-line module, submodule into importable names.
419     if module is None:
420         pass
421     elif module == 'openerp':
422         module = 'openerp.tests'
423     else:
424         module = 'openerp.addons.' + module + '.tests'
425
426     # Try to import the module
427     try:
428         __import__(module)
429     except Exception, e:
430         if explode:
431             print 'Can not `import %s`.' % module
432             import logging
433             logging.exception('')
434             sys.exit(1)
435         else:
436             if str(e) == 'No module named tests':
437                 # It seems the module has no `tests` sub-module, no problem.
438                 pass
439             else:
440                 _logger.exception('Can not `import %s`.', module)
441             return []
442
443     # Discover available test sub-modules.
444     m = sys.modules[module]
445     submodule_names =  sorted([x for x in dir(m) \
446         if x.startswith('test_') and \
447         isinstance(getattr(m, x), types.ModuleType)])
448     submodules = [getattr(m, x) for x in submodule_names]
449
450     def show_submodules_and_exit():
451         if submodule_names:
452             print 'Available submodules are:'
453             for x in submodule_names:
454                 print ' ', x
455         sys.exit(1)
456
457     if submodule is None:
458         # Use auto-discovered sub-modules.
459         ms = submodules
460     elif submodule == '__fast_suite__':
461         # Obtain the explicit test sub-modules list.
462         ms = getattr(sys.modules[module], 'fast_suite', None)
463         # `suite` was used before the 6.1 release instead of `fast_suite`.
464         ms = ms if ms else getattr(sys.modules[module], 'suite', None)
465         if ms is None:
466             if explode:
467                 print 'The module `%s` has no defined test suite.' % (module,)
468                 show_submodules_and_exit()
469             else:
470                 ms = []
471     elif submodule == '__sanity_checks__':
472         ms = getattr(sys.modules[module], 'checks', None)
473         if ms is None:
474             if explode:
475                 print 'The module `%s` has no defined sanity checks.' % (module,)
476                 show_submodules_and_exit()
477             else:
478                 ms = []
479     else:
480         # Pick the command-line-specified test sub-module.
481         m = getattr(sys.modules[module], submodule, None)
482         ms = [m]
483
484         if m is None:
485             if explode:
486                 print 'The module `%s` has no submodule named `%s`.' % \
487                     (module, submodule)
488                 show_submodules_and_exit()
489             else:
490                 ms = []
491
492     return ms
493
494 def run_unit_tests(module_name):
495     """
496     Return True or False if some tests were found and succeeded or failed.
497     Return None if no test was found.
498     """
499     import unittest2
500     ms = get_test_modules(module_name, '__fast_suite__', explode=False)
501     # TODO: No need to try again if the above call failed because of e.g. a syntax error.
502     ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False))
503     suite = unittest2.TestSuite()
504     for m in ms:
505         suite.addTests(unittest2.TestLoader().loadTestsFromModule(m))
506     if ms:
507         _test_logger.info('module %s: executing %s `fast_suite` and/or `checks` sub-modules', module_name, len(ms))
508         # Use a custom stream object to log the test executions.
509         class MyStream(object):
510             def __init__(self):
511                 self.r = re.compile(r'^-*$|^ *... *$|^ok$')
512             def flush(self):
513                 pass
514             def write(self, s):
515                 if self.r.match(s):
516                     return
517                 first = True
518                 for c in s.split('\n'):
519                     if not first:
520                         c = '` ' + c
521                     first = False
522                     _test_logger.info(c)
523         result = unittest2.TextTestRunner(verbosity=2, stream=MyStream()).run(suite)
524         if result.wasSuccessful():
525             return True
526         else:
527             _logger.error('module %s: at least one error occurred in a test', module_name)
528             return False
529
530 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: