[FIX] ListView drag&drop within parent bound.
[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.setdefault('complexity', 'normal')
253             info.setdefault('core', False)
254             info.setdefault('icon', '')
255             info['certificate'] = info.get('certificate') or None
256             info['web'] = info.get('web') or False
257             info['license'] = info.get('license') or 'AGPL-3'
258             info.setdefault('installable', True)
259             info.setdefault('active', False)
260             # If the following is provided, it is called after the module is --loaded.
261             info.setdefault('post_load', None)
262             for kind in ['data', 'demo', 'test',
263                 'init_xml', 'update_xml', 'demo_xml']:
264                 info.setdefault(kind, [])
265             return info
266
267     #TODO: refactor the logger in this file to follow the logging guidelines
268     #      for 6.0
269     logging.getLogger('modules').debug('module %s: no descriptor file'
270         ' found: __openerp__.py or __terp__.py (deprecated)', module)
271     return {}
272
273
274 def init_module_models(cr, module_name, obj_list):
275     """ Initialize a list of models.
276
277     Call _auto_init and init on each model to create or update the
278     database tables supporting the models.
279
280     TODO better explanation of _auto_init and init.
281
282     """
283     logger.notifyChannel('init', netsvc.LOG_INFO,
284         'module %s: creating or updating database tables' % module_name)
285     todo = []
286     for obj in obj_list:
287         result = obj._auto_init(cr, {'module': module_name})
288         if result:
289             todo += result
290         if hasattr(obj, 'init'):
291             obj.init(cr)
292         cr.commit()
293     for obj in obj_list:
294         obj._auto_end(cr, {'module': module_name})
295         cr.commit()
296     todo.sort()
297     for t in todo:
298         t[1](cr, *t[2])
299     cr.commit()
300
301 # Import hook to write a addon m in both sys.modules['m'] and
302 # sys.modules['openerp.addons.m']. Otherwise it could be loaded twice
303 # if imported twice using different names.
304 #class MyImportHook(object):
305 #    def find_module(self, module_name, package_path):
306 #       print ">>>", module_name, package_path
307 #    def load_module(self, module_name):
308 #       raise ImportError("Restricted")
309
310 #sys.meta_path.append(MyImportHook())
311
312 def register_module_classes(m):
313     """ Register module named m, if not already registered.
314
315     This loads the module and register all of its models, thanks to either
316     the MetaModel metaclass, or the explicit instantiation of the model.
317
318     """
319
320     def log(e):
321         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
322         msg = "Couldn't load %smodule %s" % (mt, m)
323         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
324         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
325
326     global loaded
327     if m in loaded:
328         return
329     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
330     mod_path = get_module_path(m)
331
332     initialize_sys_path()
333     try:
334         zip_mod_path = mod_path + '.zip'
335         if not os.path.isfile(zip_mod_path):
336             __import__(m)
337         else:
338             zimp = zipimport.zipimporter(zip_mod_path)
339             zimp.load_module(m)
340     except Exception, e:
341         log(e)
342         raise
343     else:
344         loaded.append(m)
345
346
347 def get_modules():
348     """Returns the list of module names
349     """
350     def listdir(dir):
351         def clean(name):
352             name = os.path.basename(name)
353             if name[-4:] == '.zip':
354                 name = name[:-4]
355             return name
356
357         def is_really_module(name):
358             name = opj(dir, name)
359             return os.path.isdir(name) or zipfile.is_zipfile(name)
360         return map(clean, filter(is_really_module, os.listdir(dir)))
361
362     plist = []
363     initialize_sys_path()
364     for ad in ad_paths:
365         plist.extend(listdir(ad))
366     return list(set(plist))
367
368
369 def get_modules_with_version():
370     modules = get_modules()
371     res = {}
372     for module in modules:
373         try:
374             info = load_information_from_description_file(module)
375             res[module] = "%s.%s" % (release.major_version, info['version'])
376         except Exception, e:
377             continue
378     return res
379
380
381 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: