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