[REF] base: Changed code to avoid overriding of context values.
[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-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 import os, sys, imp
24 from os.path import join as opj
25 import itertools
26 import zipimport
27
28 import openerp
29
30 import openerp.osv as osv
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 import openerp.pooler as pooler
35 from openerp.tools.translate import _
36
37 import openerp.netsvc as netsvc
38
39 import zipfile
40 import openerp.release as release
41
42 import re
43 import base64
44 from zipfile import PyZipFile, ZIP_DEFLATED
45 from cStringIO import StringIO
46
47 import logging
48
49 import openerp.modules.db
50 import openerp.modules.graph
51
52 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
53 ad_paths = []
54
55 # Modules already loaded
56 loaded = []
57
58 logger = netsvc.Logger()
59
60 def initialize_sys_path():
61     global ad_paths
62
63     if ad_paths:
64         return
65
66     ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(','))
67
68     sys.path.insert(1, _ad)
69
70     ad_cnt=1
71     for adp in ad_paths:
72         if adp != _ad:
73             sys.path.insert(ad_cnt, adp)
74             ad_cnt+=1
75
76     ad_paths.append(_ad)    # for get_module_path
77
78
79 def get_module_path(module, downloaded=False):
80     """Return the path of the given module.
81
82     Search the addons paths and return the first path where the given
83     module is found. If downloaded is True, return the default addons
84     path if nothing else is found.
85
86     """
87     initialize_sys_path()
88     for adp in ad_paths:
89         if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
90             return opj(adp, module)
91
92     if downloaded:
93         return opj(_ad, module)
94     logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
95     return False
96
97
98 def get_module_filetree(module, dir='.'):
99     path = get_module_path(module)
100     if not path:
101         return False
102
103     dir = os.path.normpath(dir)
104     if dir == '.':
105         dir = ''
106     if dir.startswith('..') or (dir and dir[0] == '/'):
107         raise Exception('Cannot access file outside the module')
108
109     if not os.path.isdir(path):
110         # zipmodule
111         zip = zipfile.ZipFile(path + ".zip")
112         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
113     else:
114         files = osutil.listdir(path, True)
115
116     tree = {}
117     for f in files:
118         if not f.startswith(dir):
119             continue
120
121         if dir:
122             f = f[len(dir)+int(not dir.endswith('/')):]
123         lst = f.split(os.sep)
124         current = tree
125         while len(lst) != 1:
126             current = current.setdefault(lst.pop(0), {})
127         current[lst.pop(0)] = None
128
129     return tree
130
131 def zip_directory(directory, b64enc=True, src=True):
132     """Compress a directory
133
134     @param directory: The directory to compress
135     @param base64enc: if True the function will encode the zip file with base64
136     @param src: Integrate the source files
137
138     @return: a string containing the zip file
139     """
140
141     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
142
143     def _zippy(archive, path, src=True):
144         path = os.path.abspath(path)
145         base = os.path.basename(path)
146         for f in osutil.listdir(path, True):
147             bf = os.path.basename(f)
148             if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
149                 archive.write(os.path.join(path, f), os.path.join(base, f))
150
151     archname = StringIO()
152     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
153
154     # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
155     directory = tools.ustr(directory).encode('utf-8')
156
157     archive.writepy(directory)
158     _zippy(archive, directory, src=src)
159     archive.close()
160     archive_data = archname.getvalue()
161     archname.close()
162
163     if b64enc:
164         return base64.encodestring(archive_data)
165
166     return archive_data
167
168 def get_module_as_zip(modulename, b64enc=True, src=True):
169     """Generate a module as zip file with the source or not and can do a base64 encoding
170
171     @param modulename: The module name
172     @param b64enc: if True the function will encode the zip file with base64
173     @param src: Integrate the source files
174
175     @return: a stream to store in a file-like object
176     """
177
178     ap = get_module_path(str(modulename))
179     if not ap:
180         raise Exception('Unable to find path for module %s' % modulename)
181
182     ap = ap.encode('utf8')
183     if os.path.isfile(ap + '.zip'):
184         val = file(ap + '.zip', 'rb').read()
185         if b64enc:
186             val = base64.encodestring(val)
187     else:
188         val = zip_directory(ap, b64enc, src)
189
190     return val
191
192
193 def get_module_resource(module, *args):
194     """Return the full path of a resource of the given module.
195
196     @param module: the module
197     @param args: the resource path components
198
199     @return: absolute path to the resource
200
201     TODO name it get_resource_path
202     TODO make it available inside on osv object (self.get_resource_path)
203     """
204     a = get_module_path(module)
205     if not a: return False
206     resource_path = opj(a, *args)
207     if zipfile.is_zipfile( a +'.zip') :
208         zip = zipfile.ZipFile( a + ".zip")
209         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
210         resource_path = '/'.join(args)
211         if resource_path in files:
212             return opj(a, resource_path)
213     elif os.path.exists(resource_path):
214         return resource_path
215     return False
216
217
218 def load_information_from_description_file(module):
219     """
220     :param module: The name of the module (sale, purchase, ...)
221     """
222
223     terp_file = get_module_resource(module, '__openerp__.py')
224     if not terp_file:
225         terp_file = get_module_resource(module, '__terp__.py')
226     mod_path = get_module_path(module)
227     if terp_file:
228         info = {}
229         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
230             terp_f = tools.file_open(terp_file)
231             try:
232                 info = eval(terp_f.read())
233             except Exception:
234                 logger.notifyChannel('modules', netsvc.LOG_ERROR,
235                     'module %s: exception while evaluating file %s' %
236                     (module, terp_file))
237                 raise
238             finally:
239                 terp_f.close()
240             # TODO the version should probably be mandatory
241             info.setdefault('version', '0')
242             info.setdefault('category', 'Uncategorized')
243             info.setdefault('depends', [])
244             info.setdefault('author', '')
245             info.setdefault('website', '')
246             info.setdefault('name', False)
247             info.setdefault('description', '')
248             info['certificate'] = info.get('certificate') or None
249             info['web'] = info.get('web') or False
250             info['license'] = info.get('license') or 'AGPL-3'
251             info.setdefault('installable', True)
252             info.setdefault('active', False)
253             for kind in ['data', 'demo', 'test',
254                 'init_xml', 'update_xml', 'demo_xml']:
255                 info.setdefault(kind, [])
256             return info
257
258     #TODO: refactor the logger in this file to follow the logging guidelines
259     #      for 6.0
260     logging.getLogger('modules').debug('module %s: no descriptor file'
261         ' found: __openerp__.py or __terp__.py (deprecated)', module)
262     return {}
263
264
265 def init_module_models(cr, module_name, obj_list):
266     """ Initialize a list of models.
267
268     Call _auto_init and init on each model to create or update the
269     database tables supporting the models.
270
271     TODO better explanation of _auto_init and init.
272
273     """
274
275     logger.notifyChannel('init', netsvc.LOG_INFO,
276         'module %s: creating or updating database tables' % module_name)
277     todo = []
278     for obj in obj_list:
279         result = obj._auto_init(cr, {'module': module_name})
280         if result:
281             todo += result
282         if hasattr(obj, 'init'):
283             obj.init(cr)
284         cr.commit()
285     for obj in obj_list:
286         obj._auto_end(cr, {'module': module_name})
287         cr.commit()
288     todo.sort()
289     for t in todo:
290         t[1](cr, *t[2])
291     cr.commit()
292
293
294 def load_module(module_name):
295     """ Load a Python module found on the addons paths."""
296     fm = imp.find_module(module_name, ad_paths)
297     try:
298         imp.load_module(module_name, *fm)
299     finally:
300         if fm[0]:
301             fm[0].close()
302
303
304 def register_module_classes(m):
305     """ Register module named m, if not already registered.
306
307     This will load the module and register all of its models. (Actually, the
308     explicit constructor call of each of the models inside the module will
309     register them.)
310
311     """
312
313     def log(e):
314         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
315         msg = "Couldn't load %smodule %s" % (mt, m)
316         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
317         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
318
319     global loaded
320     if m in loaded:
321         return
322     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
323     mod_path = get_module_path(m)
324
325     initialize_sys_path()
326     try:
327         zip_mod_path = mod_path + '.zip'
328         if not os.path.isfile(zip_mod_path):
329             load_module(m)
330         else:
331             zimp = zipimport.zipimporter(zip_mod_path)
332             zimp.load_module(m)
333     except Exception, e:
334         log(e)
335         raise
336     else:
337         loaded.append(m)
338
339
340 def get_modules():
341     """Returns the list of module names
342     """
343     def listdir(dir):
344         def clean(name):
345             name = os.path.basename(name)
346             if name[-4:] == '.zip':
347                 name = name[:-4]
348             return name
349
350         def is_really_module(name):
351             name = opj(dir, name)
352             return os.path.isdir(name) or zipfile.is_zipfile(name)
353         return map(clean, filter(is_really_module, os.listdir(dir)))
354
355     plist = []
356     initialize_sys_path()
357     for ad in ad_paths:
358         plist.extend(listdir(ad))
359     return list(set(plist))
360
361
362 def get_modules_with_version():
363     modules = get_modules()
364     res = {}
365     for module in modules:
366         try:
367             info = load_information_from_description_file(module)
368             res[module] = "%s.%s" % (release.major_version, info['version'])
369         except Exception, e:
370             continue
371     return res
372
373
374 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: