Launchpad automatic translations update.
[odoo/odoo.git] / bin / tools / misc.py
index 90b4c13..b45f388 100644 (file)
@@ -25,6 +25,7 @@ Miscelleanous tools used by OpenERP.
 """
 
 import inspect
+import subprocess
 import logging
 import os
 import re
@@ -33,6 +34,7 @@ import socket
 import sys
 import threading
 import time
+import warnings
 import zipfile
 from datetime import datetime
 from email.MIMEText import MIMEText
@@ -42,11 +44,16 @@ from email.Header import Header
 from email.Utils import formatdate, COMMASPACE
 from email import Encoders
 from itertools import islice, izip
+from lxml import etree
 from which import which
 if sys.version_info[:2] < (2, 4):
     from threadinglocal import local
 else:
     from threading import local
+try:
+    from html2text import html2text
+except ImportError:
+    html2text = None
 
 import netsvc
 from config import config
@@ -54,12 +61,20 @@ from lru import LRU
 
 _logger = logging.getLogger('tools')
 
+# List of etree._Element subclasses that we choose to ignore when parsing XML.
+# We include the *Base ones just in case, currently they seem to be subclasses of the _* ones.
+SKIPPED_ELEMENT_TYPES = (etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase)
+
 # initialize a database with base/base.sql
 def init_db(cr):
     import addons
     f = addons.get_module_resource('base', 'base.sql')
-    cr.execute(file_open(f).read())
-    cr.commit()
+    base_sql_file = file_open(f)
+    try:
+        cr.execute(base_sql_file.read())
+        cr.commit()
+    finally:
+        base_sql_file.close()
 
     for i in addons.get_modules():
         mod_path = addons.get_module_path(i)
@@ -74,20 +89,19 @@ def init_db(cr):
         p_id = None
         while categs:
             if p_id is not None:
-                cr.execute('select id \
-                           from ir_module_category \
-                           where name=%s and parent_id=%s', (categs[0], p_id))
+                cr.execute('SELECT id \
+                           FROM ir_module_category \
+                           WHERE name=%s AND parent_id=%s', (categs[0], p_id))
             else:
-                cr.execute('select id \
-                           from ir_module_category \
-                           where name=%s and parent_id is NULL', (categs[0],))
+                cr.execute('SELECT id \
+                           FROM ir_module_category \
+                           WHERE name=%s AND parent_id IS NULL', (categs[0],))
             c_id = cr.fetchone()
             if not c_id:
-                cr.execute('select nextval(\'ir_module_category_id_seq\')')
+                cr.execute('INSERT INTO ir_module_category \
+                        (name, parent_id) \
+                        VALUES (%s, %s) RETURNING id', (categs[0], p_id))
                 c_id = cr.fetchone()[0]
-                cr.execute('insert into ir_module_category \
-                        (id, name, parent_id) \
-                        values (%s, %s, %s)', (c_id, categs[0], p_id))
             else:
                 c_id = c_id[0]
             p_id = c_id
@@ -102,23 +116,23 @@ def init_db(cr):
                 state = 'uninstalled'
         else:
             state = 'uninstallable'
-        cr.execute('select nextval(\'ir_module_module_id_seq\')')
-        id = cr.fetchone()[0]
-        cr.execute('insert into ir_module_module \
-                (id, author, website, name, shortdesc, description, \
-                    category_id, state, certificate, web) \
-                values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)', (
-            id, info.get('author', ''),
+        cr.execute('INSERT INTO ir_module_module \
+                (author, website, name, shortdesc, description, \
+                    category_id, state, certificate, web, license) \
+                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
+            info.get('author', ''),
             info.get('website', ''), i, info.get('name', False),
             info.get('description', ''), p_id, state, info.get('certificate') or None,
-            info.get('web') or False))
-        cr.execute('insert into ir_model_data \
-            (name,model,module, res_id, noupdate) values (%s,%s,%s,%s,%s)', (
+            info.get('web') or False,
+            info.get('license') or 'AGPL-3'))
+        id = cr.fetchone()[0]
+        cr.execute('INSERT INTO ir_model_data \
+            (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
                 'module_meta_information', 'ir.module.module', i, id, True))
         dependencies = info.get('depends', [])
         for d in dependencies:
-            cr.execute('insert into ir_module_module_dependency \
-                    (module_id,name) values (%s, %s)', (id, d))
+            cr.execute('INSERT INTO ir_module_module_dependency \
+                    (module_id,name) VALUES (%s, %s)', (id, d))
         cr.commit()
 
 def find_in_path(name):
@@ -140,28 +154,31 @@ def exec_pg_command(name, *args):
     prog = find_pg_tool(name)
     if not prog:
         raise Exception('Couldn\'t find %s' % name)
-    args2 = (os.path.basename(prog),) + args
-    return os.spawnv(os.P_WAIT, prog, args2)
+    args2 = (prog,) + args
+    
+    return subprocess.call(args2)
 
 def exec_pg_command_pipe(name, *args):
     prog = find_pg_tool(name)
     if not prog:
         raise Exception('Couldn\'t find %s' % name)
-    if os.name == "nt":
-        cmd = '"' + prog + '" ' + ' '.join(args)
-    else:
-        cmd = prog + ' ' + ' '.join(args)
-    return os.popen2(cmd, 'b')
+    # on win32, passing close_fds=True is not compatible
+    # with redirecting std[in/err/out]
+    pop = subprocess.Popen((prog,) + args, bufsize= -1,
+          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+          close_fds=(os.name=="posix"))
+    return (pop.stdin, pop.stdout)
 
 def exec_command_pipe(name, *args):
     prog = find_in_path(name)
     if not prog:
         raise Exception('Couldn\'t find %s' % name)
-    if os.name == "nt":
-        cmd = '"'+prog+'" '+' '.join(args)
-    else:
-        cmd = prog+' '+' '.join(args)
-    return os.popen2(cmd, 'b')
+    # on win32, passing close_fds=True is not compatible
+    # with redirecting std[in/err/out]
+    pop = subprocess.Popen((prog,) + args, bufsize= -1,
+          stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+          close_fds=(os.name=="posix"))
+    return (pop.stdin, pop.stdout)
 
 #----------------------------------------------------------
 # File paths
@@ -255,7 +272,7 @@ def file_open(name, mode="r", subdir='addons', pathinfo=False):
             return fo
     if os.path.splitext(name)[1] == '.rml':
         raise IOError, 'Report %s doesn\'t exist or deleted : ' %str(name)
-    raise IOError, 'File not found : '+str(name)
+    raise IOError, 'File not found : %s' % name
 
 
 #----------------------------------------------------------
@@ -501,8 +518,7 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
     email_body = ustr(body).encode('utf-8')
     email_text = MIMEText(email_body or '',_subtype=subtype,_charset='utf-8')
 
-    if attach: msg = MIMEMultipart()
-    else: msg = email_text
+    msg = MIMEMultipart()
 
     msg['Subject'] = Header(ustr(subject), 'utf-8')
     msg['From'] = email_from
@@ -514,8 +530,6 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
     msg['To'] = COMMASPACE.join(email_to)
     if email_cc:
         msg['Cc'] = COMMASPACE.join(email_cc)
-    if email_bcc:
-        msg['Bcc'] = COMMASPACE.join(email_bcc)
     msg['Date'] = formatdate(localtime=True)
 
     msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
@@ -524,8 +538,16 @@ def email_send(email_from, email_to, subject, body, email_cc=None, email_bcc=Non
     for key, value in x_headers.iteritems():
         msg['%s' % key] = str(value)
 
-    if attach:
+    if html2text and subtype == 'html':
+        text = html2text(email_body.decode('utf-8')).encode('utf-8')
+        alternative_part = MIMEMultipart(_subtype="alternative")
+        alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
+        alternative_part.attach(email_text)
+        msg.attach(alternative_part)
+    else:
         msg.attach(email_text)
+
+    if attach:
         for (fname,fcontent) in attach:
             part = MIMEBase('application', "octet-stream")
             part.set_payload( fcontent )
@@ -914,6 +936,8 @@ def get_iso_codes(lang):
     return lang
 
 def get_languages():
+    # The codes below are those from Launchpad's Rosetta, with the exception
+    # of some trivial codes where the Launchpad code is xx and we have xx_XX.
     languages={
         'ab_RU': u'Abkhazian / аҧсуа',
         'ar_AR': u'Arabic / الْعَرَبيّة',
@@ -954,6 +978,7 @@ def get_languages():
         'fr_FR': u'French / Français',
         'gl_ES': u'Galician / Galego',
         'gu_IN': u'Gujarati / ગુજરાતી',
+        'he_IL': u'Hebrew / עִבְרִי',
         'hi_IN': u'Hindi / हिंदी',
         'hr_HR': u'Croatian / hrvatski jezik',
         'hu_HU': u'Hungarian / Magyar',
@@ -980,7 +1005,8 @@ def get_languages():
         'sl_SI': u'Slovenian / slovenščina',
         'sk_SK': u'Slovak / Slovenský jazyk',
         'sq_AL': u'Albanian / Shqip',
-        'sr_RS': u'Serbian / српски језик',
+        'sr_RS': u'Serbian (Cyrillic) / српски',
+        'sr@latin': u'Serbian (Latin) / srpski',
         'sv_SE': u'Swedish / svenska',
         'te_IN': u'Telugu / తెలుగు',
         'tr_TR': u'Turkish / Türkçe',
@@ -1114,6 +1140,8 @@ def debug(what):
             --log-level=debug
 
     """
+    warnings.warn("The tools.debug() method is deprecated, please use logging.",
+                      DeprecationWarning, stacklevel=2)
     from inspect import stack
     from pprint import pformat
     st = stack()[1]
@@ -1122,10 +1150,10 @@ def debug(what):
     what = pformat(what)
     if param != what:
         what = "%s = %s" % (param, what)
-    netsvc.Logger().notifyChannel(st[3], netsvc.LOG_DEBUG, what)
+    logging.getLogger(st[3]).debug(what)
 
 
-icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
+__icons_list = ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',
 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER',
 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE',
 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO',
@@ -1161,7 +1189,11 @@ icons = map(lambda x: (x,x), ['STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_
 'terp-gdu-smart-failing','terp-go-week','terp-gtk-select-all','terp-locked','terp-mail-forward',
 'terp-mail-message-new','terp-mail-replied','terp-rating-rated','terp-stage','terp-stock_format-scientific',
 'terp-dolar_ok!','terp-idea','terp-stock_format-default','terp-mail-','terp-mail_delete'
-])
+]
+
+def icons(*a, **kw):
+    global __icons_list
+    return [(x, x) for x in __icons_list ]
 
 def extract_zip_file(zip_file, outdirectory):
     zf = zipfile.ZipFile(zip_file, 'r')
@@ -1324,6 +1356,51 @@ DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
     DEFAULT_SERVER_DATE_FORMAT,
     DEFAULT_SERVER_TIME_FORMAT)
 
+# Python's strftime supports only the format directives
+# that are available on the platform's libc, so in order to
+# be cross-platform we map to the directives required by
+# the C standard (1989 version), always available on platforms
+# with a C standard implementation.
+DATETIME_FORMATS_MAP = {
+        '%C': '', # century
+        '%D': '%m/%d/%Y', # modified %y->%Y
+        '%e': '%d',
+        '%E': '', # special modifier
+        '%F': '%Y-%m-%d',
+        '%g': '%Y', # modified %y->%Y
+        '%G': '%Y',
+        '%h': '%b',
+        '%k': '%H',
+        '%l': '%I',
+        '%n': '\n',
+        '%O': '', # special modifier
+        '%P': '%p',
+        '%R': '%H:%M',
+        '%r': '%I:%M:%S %p',
+        '%s': '', #num of seconds since epoch
+        '%T': '%H:%M:%S',
+        '%t': ' ', # tab
+        '%u': ' %w',
+        '%V': '%W',
+        '%y': '%Y', # Even if %y works, it's ambiguous, so we should use %Y
+        '%+': '%Y-%m-%d %H:%M:%S',
+
+        # %Z is a special case that causes 2 problems at least:
+        #  - the timezone names we use (in res_user.context_tz) come
+        #    from pytz, but not all these names are recognized by
+        #    strptime(), so we cannot convert in both directions
+        #    when such a timezone is selected and %Z is in the format
+        #  - %Z is replaced by an empty string in strftime() when
+        #    there is not tzinfo in a datetime value (e.g when the user
+        #    did not pick a context_tz). The resulting string does not
+        #    parse back if the format requires %Z.
+        # As a consequence, we strip it completely from format strings.
+        # The user can always have a look at the context_tz in
+        # preferences to check the timezone.
+        '%z': '',
+        '%Z': '',
+}
+
 def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
         tz_offset=True, ignore_unparsable_time=True):
     """