[FIX] Set a default value if the x_headers argument has the value 'None'
[odoo/odoo.git] / bin / tools / misc.py
index 183f172..1ead9ea 100644 (file)
@@ -2,7 +2,7 @@
 ##############################################################################
 #
 #    OpenERP, Open Source Management Solution
-#    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
+#    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
 #    $Id$
 #
 #    This program is free software: you can redistribute it and/or modify
@@ -44,7 +44,7 @@ from itertools import izip
 def init_db(cr):
     import addons
     f = addons.get_module_resource('base', 'base.sql')
-    for line in file(f).read().split(';'):
+    for line in file_open(f).read().split(';'):
         if (len(line)>0) and (not line.isspace()):
             cr.execute(line)
     cr.commit()
@@ -55,12 +55,8 @@ def init_db(cr):
         if not mod_path:
             continue
         info = False
-        if os.path.isfile(terp_file) and not os.path.isfile(mod_path+'.zip'):
-            info = eval(file(terp_file).read())
-        elif zipfile.is_zipfile(mod_path+'.zip'):
-            zfile = zipfile.ZipFile(mod_path+'.zip')
-            i = os.path.splitext(i)[0]
-            info = eval(zfile.read(os.path.join(i, '__terp__.py')))
+        if os.path.isfile(terp_file) or os.path.isfile(mod_path+'.zip'):
+            info = eval(file_open(terp_file).read())
         if info:
             categs = info.get('category', 'Uncategorized').split('/')
             p_id = None
@@ -103,6 +99,9 @@ def init_db(cr):
                 id, info.get('author', ''),
                 info.get('website', ''), i, info.get('name', False),
                 info.get('description', ''), p_id, state))
+            cr.execute('insert into ir_model_data \
+                (name,model,module, res_id) values (%s,%s,%s,%s)', (
+                    'module_meta_information', 'ir.module.module', i, id))
             dependencies = info.get('depends', [])
             for d in dependencies:
                 cr.execute('insert into ir_module_module_dependency \
@@ -224,13 +223,14 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
         else:
             zipname = tail
         if zipfile.is_zipfile(head+'.zip'):
-            import StringIO
+            from cStringIO import StringIO
             zfile = zipfile.ZipFile(head+'.zip')
             try:
-                fo = StringIO.StringIO(zfile.read(os.path.join(
+                fo = StringIO()
+                fo.write(zfile.read(os.path.join(
                     os.path.basename(head), zipname).replace(
                         os.sep, '/')))
-
+                fo.seek(0)
                 if pathinfo:
                     return fo, name
                 return fo
@@ -302,7 +302,8 @@ def reverse_enumerate(l):
 #----------------------------------------------------------
 # Emails
 #----------------------------------------------------------
-def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attach=None, tinycrm=False, ssl=False, debug=False,subtype='plain'):
+def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
+               attach=None, tinycrm=False, ssl=False, debug=False, subtype='plain', x_headers=None):
     """Send an email."""
     import smtplib
     from email.MIMEText import MIMEText
@@ -313,9 +314,15 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
     from email.Utils import formatdate, COMMASPACE
     from email import Encoders
 
+    if x_headers is None:
+        x_headers = {}
+
     if not ssl:
         ssl = config.get('smtp_ssl', False)
 
+    if not email_from and not config['email_from']:
+        raise Exception("No Email sender by default, see config file")
+
     if not email_cc:
         email_cc = []
     if not email_bcc:
@@ -326,11 +333,13 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
     else:
         msg = MIMEMultipart()
 
-    msg['Subject'] = Header(subject.decode('utf8'), 'utf-8')
+    msg['Subject'] = Header(ustr(subject), 'utf-8')
     msg['From'] = email_from
     del msg['Reply-To']
     if reply_to:
-        msg['Reply-To'] = msg['From']+', '+reply_to
+        msg['Reply-To'] = reply_to
+    else:
+        msg['Reply-To'] = msg['From']
     msg['To'] = COMMASPACE.join(email_to)
     if email_cc:
         msg['Cc'] = COMMASPACE.join(email_cc)
@@ -338,6 +347,15 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
         msg['Bcc'] = COMMASPACE.join(email_bcc)
     msg['Date'] = formatdate(localtime=True)
 
+    # Add OpenERP Server information
+    msg['X-Generated-By'] = 'OpenERP (http://www.openerp.com)'
+    msg['X-OpenERP-Server-Host'] = socket.gethostname()
+    msg['X-OpenERP-Server-Version'] = release.version
+
+    # Add dynamic X Header
+    for key, value in x_headers.items():
+        msg['X-OpenERP-%s' % key] = str(value)
+
     if tinycrm:
         msg['Message-Id'] = "<%s-tinycrm-%s@%s>" % (time.time(), tinycrm, socket.gethostname())
 
@@ -381,11 +399,11 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
 # text must be latin-1 encoded
 def sms_send(user, password, api_id, text, to):
     import urllib
-    params = urllib.urlencode({'user': user, 'password': password, 'api_id': api_id, 'text': text, 'to':to})
-    #f = urllib.urlopen("http://api.clickatell.com/http/sendmsg", params)
-    f = urllib.urlopen("http://196.7.150.220/http/sendmsg", params)
+    url = "http://api.urlsms.com/SendSMS.aspx"
+    #url = "http://196.7.150.220/http/sendmsg"
+    params = urllib.urlencode({'UserID': user, 'Password': password, 'SenderID': api_id, 'MsgText': text, 'RecipientMobileNo':to})
+    f = urllib.urlopen(url+"?"+params)
     # FIXME: Use the logger if there is an error
-    print f.read()
     return True
 
 #---------------------------------------------------------
@@ -532,49 +550,128 @@ def is_hashable(h):
     except TypeError:
         return False
 
-#
-# Use it as a decorator of the function you plan to cache
-# Timeout: 0 = no timeout, otherwise in seconds
-#
 class cache(object):
-    def __init__(self, timeout=10000, skiparg=2):
-        self.timeout = timeout
+    """
+    Use it as a decorator of the function you plan to cache
+    Timeout: 0 = no timeout, otherwise in seconds
+    """
+    
+    __caches = []
+    
+    def __init__(self, timeout=None, skiparg=2, multi=None):
+        assert skiparg >= 2 # at least self and cr
+        if timeout is None:
+            self.timeout = config['cache_timeout']
+        else:
+            self.timeout = timeout
+        self.skiparg = skiparg
+        self.multi = multi
+        self.lasttime = time.time()
         self.cache = {}
+        self.fun = None 
+        cache.__caches.append(self)
+
+    
+    def _generate_keys(self, dbname, kwargs2):
+        """
+        Generate keys depending of the arguments and the self.mutli value
+        """
+        
+        def to_tuple(d):
+            i = d.items()
+            i.sort(key=lambda (x,y): x)
+            return tuple(i)
+
+        if not self.multi:
+            key = (('dbname', dbname),) + to_tuple(kwargs2)
+            yield key, None
+        else:
+            multis = kwargs2[self.multi][:]    
+            for id in multis:
+                kwargs2[self.multi] = (id,)
+                key = (('dbname', dbname),) + to_tuple(kwargs2)
+                yield key, id
+    
+    def _unify_args(self, *args, **kwargs):
+        # Update named arguments with positional argument values (without self and cr)
+        kwargs2 = self.fun_default_values.copy()
+        kwargs2.update(kwargs)
+        kwargs2.update(dict(zip(self.fun_arg_names, args[self.skiparg-2:])))
+        for k in kwargs2:
+            if isinstance(kwargs2[k], (list, dict, set)):
+                kwargs2[k] = tuple(kwargs2[k])
+            elif not is_hashable(kwargs2[k]):
+                kwargs2[k] = repr(kwargs2[k])
+
+        return kwargs2
+    
+    def clear(self, dbname, *args, **kwargs):
+        """clear the cache for database dbname
+            if *args and **kwargs are both empty, clear all the keys related to this database
+        """
+        if not args and not kwargs:
+            keys_to_del = [key for key in self.cache if key[0][1] == dbname]
+        else:
+            kwargs2 = self._unify_args(*args, **kwargs)
+            keys_to_del = [key for key, _ in self._generate_keys(dbname, kwargs2) if key in self.cache]
+        
+        for key in keys_to_del:
+            del self.cache[key]
+    
+    @classmethod
+    def clean_caches_for_db(cls, dbname):
+        for c in cls.__caches:
+            c.clear(dbname)
 
     def __call__(self, fn):
-        arg_names = inspect.getargspec(fn)[0][2:]
-        def cached_result(self2, cr=None, *args, **kwargs):
-            if cr is None:
-                self.cache = {}
-                return True
-
-            # Update named arguments with positional argument values
-            kwargs.update(dict(zip(arg_names, args)))
-            for k in kwargs:
-                if isinstance(kwargs[k], (list, dict, set)):
-                    kwargs[k] = tuple(kwargs[k])
-                elif not is_hashable(kwargs[k]):
-                    kwargs[k] = repr(kwargs[k])
-            kwargs = kwargs.items()
-            kwargs.sort()
-
-            # Work out key as a tuple of ('argname', value) pairs
-            key = (('dbname', cr.dbname),) + tuple(kwargs)
-
-            # Check cache and return cached value if possible
-            if key in self.cache:
-                (value, last_time) = self.cache[key]
-                mintime = time.time() - self.timeout
-                if self.timeout <= 0 or mintime <= last_time:
-                    return value
-
-            # Work out new value, cache it and return it
-            # FIXME Should copy() this value to avoid futur modifications of the cache ?
-            # FIXME What about exceptions ?
-            result = fn(self2,cr,**dict(kwargs))
-
-            self.cache[key] = (result, time.time())
+        if self.fun is not None:
+            raise Exception("Can not use a cache instance on more than one function")
+        self.fun = fn
+
+        argspec = inspect.getargspec(fn)
+        self.fun_arg_names = argspec[0][self.skiparg:]
+        self.fun_default_values = {}
+        if argspec[3]:
+            self.fun_default_values = dict(zip(self.fun_arg_names[-len(argspec[3]):], argspec[3]))
+        
+        def cached_result(self2, cr, *args, **kwargs):
+            if time.time()-self.timeout > self.lasttime:
+                self.lasttime = time.time()
+                t = time.time()-self.timeout 
+                for key in self.cache.keys():
+                    if self.cache[key][1]<t:
+                        del self.cache[key]
+
+            kwargs2 = self._unify_args(*args, **kwargs)
+
+            result = {}
+            notincache = {}
+            for key, id in self._generate_keys(cr.dbname, kwargs2):
+                if key in self.cache:
+                    result[id] = self.cache[key][0]
+                else:
+                    notincache[id] = key
+            
+            if notincache:
+                if self.multi:
+                    kwargs2[self.multi] = notincache.keys()
+                
+                result2 = fn(self2, cr, *args[2:self.skiparg], **kwargs2)
+                if not self.multi:
+                    key = notincache[None]
+                    self.cache[key] = (result2, time.time())
+                    result[None] = result2
+                else:
+                    for id in result2:
+                        key = notincache[id]
+                        self.cache[key] = (result2[id], time.time())
+                    result.update(result2)
+                        
+            if not self.multi:
+                return result[None]
             return result
+
+        cached_result.clear_cache = self.clear
         return cached_result
 
 def to_xml(s):
@@ -599,36 +696,90 @@ def ustr(value):
     if not isinstance(value, str):
         value = str(value)
 
-    return unicode(value, 'utf-8')
+    try: # first try utf-8
+        return unicode(value, 'utf-8')
+    except:
+        pass
+
+    try: # then extened iso-8858
+        return unicode(value, 'iso-8859-15')
+    except:
+        pass
+
+    # else use default system locale
+    from locale import getlocale
+    return unicode(value, getlocale()[1])
+
+def exception_to_unicode(e):
+    if hasattr(e, 'message'):
+        return ustr(e.message)
+    if hasattr(e, 'args'):
+        return "\n".join((ustr(a) for a in e.args))
+    try:
+        return ustr(e)
+    except:
+        return u"Unknow message"
+
+
+# to be compatible with python 2.4
+import __builtin__
+if not hasattr(__builtin__, 'all'):
+    def all(iterable):
+        for element in iterable:
+            if not element:
+               return False
+        return True
+        
+    __builtin__.all = all
+    del all
+    
+if not hasattr(__builtin__, 'any'):
+    def any(iterable):
+        for element in iterable:
+            if element:
+               return True
+        return False
+        
+    __builtin__.any = any
+    del any
+
 
 
 def get_languages():
     languages={
+        'ar_AR': u'Arabic / الْعَرَبيّة',
         'bg_BG': u'Bulgarian / български',
+        'bs_BS': u'Bosnian / bosanski jezik',
         'ca_ES': u'Catalan / Català',
         'cs_CZ': u'Czech / Čeština',
+        'da_DK': u'Danish / Dansk',
         'de_DE': u'German / Deutsch',
+        'el_EL': u'Greek / Ελληνικά',
         'en_CA': u'English (CA)',
         'en_EN': u'English (default)',
         'en_GB': u'English (UK)',
         'en_US': u'English (US)',
         'es_AR': u'Spanish (AR) / Español (AR)',
         'es_ES': u'Spanish / Español',
-        'et_ET': u'Estonian / Eesti keel',
+        'et_EE': u'Estonian / Eesti keel',
         'fr_BE': u'French (BE) / Français (BE)',
         'fr_CH': u'French (CH) / Français (CH)',
         'fr_FR': u'French / Français',
         'hr_HR': u'Croatian / hrvatski jezik',
         'hu_HU': u'Hungarian / Magyar',
+        'id_ID': u'Indonesian / Bahasa Indonesia',
         'it_IT': u'Italian / Italiano',
         'lt_LT': u'Lithuanian / Lietuvių kalba',
         'nl_NL': u'Dutch / Nederlands',
+        'nl_BE': u'Dutch (Belgium) / Nederlands (Belgïe)',
+        'pl_PL': u'Polish / Język polski',
         'pt_BR': u'Portugese (BR) / português (BR)',
         'pt_PT': u'Portugese / português',
         'ro_RO': u'Romanian / limba română',
         'ru_RU': u'Russian / русский язык',
         'sl_SL': u'Slovenian / slovenščina',
         'sv_SE': u'Swedish / svenska',
+        'tr_TR': u'Turkish / Türkçe',
         'uk_UK': u'Ukrainian / украї́нська мо́ва',
         'zh_CN': u'Chinese (CN) / 简体中文' ,
         'zh_TW': u'Chinese (TW) / 正體字',
@@ -688,40 +839,85 @@ def human_size(sz):
         i = i + 1
     return "%0.2f %s" % (s, units[i])
 
-def logged(when):
-    import netsvc
-    def log(f, res, *args, **kwargs):
-        vector = ['Call -> function: %s' % f]
+def logged(f):
+    from tools.func import wraps
+    
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        import netsvc
+        from pprint import pformat
+
+        vector = ['Call -> function: %r' % f]
         for i, arg in enumerate(args):
-            vector.append( '  arg %02d: %r' % ( i, arg ) )
+            vector.append('  arg %02d: %s' % (i, pformat(arg)))
         for key, value in kwargs.items():
-            vector.append( '  kwarg %10s: %r' % ( key, value ) )
-        vector.append( '  result: %r' % res )
-        netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, vector)
+            vector.append('  kwarg %10s: %s' % (key, pformat(value)))
+
+        timeb4 = time.time()
+        res = f(*args, **kwargs)
+        
+        vector.append('  result: %s' % pformat(res))
+        vector.append('  time delta: %s' % (time.time() - timeb4))
+        netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, '\n'.join(vector))
+        return res
 
-    def pre_logged(f):
-        def wrapper(*args, **kwargs):
-            res = f(*args, **kwargs)
-            log(f, res, *args, **kwargs)
-            return res
-        return wrapper
+    return wrapper
 
-    def post_logged(f):
+class profile(object):
+    def __init__(self, fname=None):
+        self.fname = fname
+
+    def __call__(self, f):
+        from tools.func import wraps
+
+        @wraps(f)
         def wrapper(*args, **kwargs):
-            now = time.time()
-            res = None
-            try:
-                res = f(*args, **kwargs)
-                return res
-            finally:
-                log(f, res, *args, **kwargs)
-                netsvc.Logger().notifyChannel('logged', netsvc.LOG_DEBUG, "time delta: %s" % (time.time() - now))
+            class profile_wrapper(object):
+                def __init__(self):
+                    self.result = None
+                def __call__(self):
+                    self.result = f(*args, **kwargs)
+            pw = profile_wrapper()
+            import cProfile
+            fname = self.fname or ("%s.cprof" % (f.func_name,))
+            cProfile.runctx('pw()', globals(), locals(), filename=fname)
+            return pw.result
+
         return wrapper
 
-    try:
-        return { "pre" : pre_logged, "post" : post_logged}[when]
-    except KeyError, e:
-        raise ValueError(e), "must to be 'pre' or 'post'"
+def debug(what):
+    """
+        This method allow you to debug your code without print
+        Example:
+        >>> def func_foo(bar)
+        ...     baz = bar
+        ...     debug(baz)
+        ...     qnx = (baz, bar)
+        ...     debug(qnx)
+        ...
+        >>> func_foo(42)
+
+        This will output on the logger:
+        
+            [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:baz = 42
+            [Wed Dec 25 00:00:00 2008] DEBUG:func_foo:qnx = (42, 42)
+
+        To view the DEBUG lines in the logger you must start the server with the option
+            --log-level=debug
+
+    """
+    import netsvc
+    from inspect import stack
+    import re
+    from pprint import pformat
+    st = stack()[1]
+    param = re.split("debug *\((.+)\)", st[4][0].strip())[1].strip()
+    while param.count(')') > param.count('('): param = param[:param.rfind(')')]
+    what = pformat(what)
+    if param != what:
+        what = "%s = %s" % (param, what)
+    netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
+
 
 icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
@@ -751,6 +947,26 @@ icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_
 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph',
 ])
 
+def extract_zip_file(zip_file, outdirectory):
+    import zipfile
+    import os
+
+    zf = zipfile.ZipFile(zip_file, 'r')
+    out = outdirectory
+    for path in zf.namelist():
+        tgt = os.path.join(out, path)
+        tgtdir = os.path.dirname(tgt)
+        if not os.path.exists(tgtdir):
+            os.makedirs(tgtdir)
+
+        if not tgt.endswith(os.sep):
+            fp = open(tgt, 'wb')
+            fp.write(zf.read(path))
+            fp.close()
+    zf.close()
+
+
+
 
 
 if __name__ == '__main__':