[IMP] orm: introduce cleaner class hierarchy for models
[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         if obj._transient:
288             obj.vacuum(cr, openerp.SUPERUSER)
289         cr.commit()
290     todo.sort()
291     for t in todo:
292         t[1](cr, *t[2])
293     cr.commit()
294
295
296 def load_module(module_name):
297     """ Load a Python module found on the addons paths."""
298     fm = imp.find_module(module_name, ad_paths)
299     try:
300         imp.load_module(module_name, *fm)
301     finally:
302         if fm[0]:
303             fm[0].close()
304
305
306 def register_module_classes(m):
307     """ Register module named m, if not already registered.
308
309     This will load the module and register all of its models. (Actually, the
310     explicit constructor call of each of the models inside the module will
311     register them.)
312
313     """
314
315     def log(e):
316         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
317         msg = "Couldn't load %smodule %s" % (mt, m)
318         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
319         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
320
321     global loaded
322     if m in loaded:
323         return
324     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
325     mod_path = get_module_path(m)
326
327     initialize_sys_path()
328     try:
329         zip_mod_path = mod_path + '.zip'
330         if not os.path.isfile(zip_mod_path):
331             load_module(m)
332         else:
333             zimp = zipimport.zipimporter(zip_mod_path)
334             zimp.load_module(m)
335     except Exception, e:
336         log(e)
337         raise
338     else:
339         loaded.append(m)
340
341
342 def get_modules():
343     """Returns the list of module names
344     """
345     def listdir(dir):
346         def clean(name):
347             name = os.path.basename(name)
348             if name[-4:] == '.zip':
349                 name = name[:-4]
350             return name
351
352         def is_really_module(name):
353             name = opj(dir, name)
354             return os.path.isdir(name) or zipfile.is_zipfile(name)
355         return map(clean, filter(is_really_module, os.listdir(dir)))
356
357     plist = []
358     initialize_sys_path()
359     for ad in ad_paths:
360         plist.extend(listdir(ad))
361     return list(set(plist))
362
363
364 def get_modules_with_version():
365     modules = get_modules()
366     res = {}
367     for module in modules:
368         try:
369             info = load_information_from_description_file(module)
370             res[module] = "%s.%s" % (release.major_version, info['version'])
371         except Exception, e:
372             continue
373     return res
374
375
376 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: