[MERGE] forward port of branch saas-3 up to d36eee9
authorChristophe Simonis <chs@odoo.com>
Wed, 5 Nov 2014 18:34:53 +0000 (19:34 +0100)
committerChristophe Simonis <chs@odoo.com>
Wed, 5 Nov 2014 19:26:36 +0000 (20:26 +0100)
1  2 
openerp/tools/misc.py
openerp/tools/translate.py

diff --combined openerp/tools/misc.py
@@@ -3,7 -3,7 +3,7 @@@
  #
  #    OpenERP, Open Source Management Solution
  #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
 -#    Copyright (C) 2010-2013 OpenERP s.a. (<http://openerp.com>).
 +#    Copyright (C) 2010-2014 OpenERP s.a. (<http://openerp.com>).
  #
  #    This program is free software: you can redistribute it and/or modify
  #    it under the terms of the GNU Affero General Public License as
@@@ -27,7 -27,6 +27,7 @@@ Miscellaneous tools used by OpenERP
  
  from functools import wraps
  import cProfile
 +from contextlib import contextmanager
  import subprocess
  import logging
  import os
@@@ -35,7 -34,6 +35,7 @@@ import socke
  import sys
  import threading
  import time
 +import werkzeug.utils
  import zipfile
  from collections import defaultdict, Mapping
  from datetime import datetime
@@@ -52,7 -50,6 +52,7 @@@ except ImportError
  
  from config import config
  from cache import *
 +from .parse_version import parse_version 
  
  import openerp
  # get_encodings, ustr and exception_to_unicode were originally from tools.misc.
@@@ -86,8 -83,7 +86,8 @@@ def exec_pg_command(name, *args)
          raise Exception('Couldn\'t find %s' % name)
      args2 = (prog,) + args
  
 -    return subprocess.call(args2)
 +    with open(os.devnull) as dn:
 +        return subprocess.call(args2, stdout=dn, stderr=subprocess.STDOUT)
  
  def exec_pg_command_pipe(name, *args):
      prog = find_pg_tool(name)
@@@ -278,36 -274,6 +278,36 @@@ def reverse_enumerate(l)
      """
      return izip(xrange(len(l)-1, -1, -1), reversed(l))
  
 +def topological_sort(elems):
 +    """ Return a list of elements sorted so that their dependencies are listed
 +    before them in the result.
 +
 +    :param elems: specifies the elements to sort with their dependencies; it is
 +        a dictionary like `{element: dependencies}` where `dependencies` is a
 +        collection of elements that must appear before `element`. The elements
 +        of `dependencies` are not required to appear in `elems`; they will
 +        simply not appear in the result.
 +
 +    :returns: a list with the keys of `elems` sorted according to their
 +        specification.
 +    """
 +    # the algorithm is inspired by [Tarjan 1976],
 +    # http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
 +    result = []
 +    visited = set()
 +
 +    def visit(n):
 +        if n not in visited:
 +            visited.add(n)
 +            if n in elems:
 +                # first visit all dependencies of n, then append n to result
 +                map(visit, elems[n])
 +                result.append(n)
 +
 +    map(visit, elems)
 +
 +    return result
 +
  
  class UpdateableStr(local):
      """ Class that stores an updateable string (used in wizards)
@@@ -460,7 -426,7 +460,7 @@@ ALL_LANGUAGES = 
          'am_ET': u'Amharic / አምሃርኛ',
          'ar_SY': u'Arabic / الْعَرَبيّة',
          'bg_BG': u'Bulgarian / български език',
-         'bs_BS': u'Bosnian / bosanski jezik',
+         'bs_BA': u'Bosnian / bosanski jezik',
          'ca_ES': u'Catalan / Català',
          'cs_CZ': u'Czech / Čeština',
          'da_DK': u'Danish / Dansk',
          'fa_IR': u'Persian / فارس',
          'fi_FI': u'Finnish / Suomi',
          'fr_BE': u'French (BE) / Français (BE)',
 +        'fr_CA': u'French (CA) / Français (CA)',
          'fr_CH': u'French (CH) / Français (CH)',
          'fr_FR': u'French / Français',
          'gl_ES': u'Galician / Galego',
@@@ -1206,7 -1171,7 +1206,7 @@@ class ConstantMapping(Mapping)
          return self._value
  
  
 -def dumpstacks(sig, frame):
 +def dumpstacks(sig=None, frame=None):
      """ Signal handler: dump a stack trace for each existing thread."""
      code = []
  
  
      _logger.info("\n".join(code))
  
 -
 +class frozendict(dict):
 +    """ An implementation of an immutable dictionary. """
 +    def __delitem__(self, key):
 +        raise NotImplementedError("'__delitem__' not supported on frozendict")
 +    def __setitem__(self, key, val):
 +        raise NotImplementedError("'__setitem__' not supported on frozendict")
 +    def clear(self):
 +        raise NotImplementedError("'clear' not supported on frozendict")
 +    def pop(self, key, default=None):
 +        raise NotImplementedError("'pop' not supported on frozendict")
 +    def popitem(self):
 +        raise NotImplementedError("'popitem' not supported on frozendict")
 +    def setdefault(self, key, default=None):
 +        raise NotImplementedError("'setdefault' not supported on frozendict")
 +    def update(self, *args, **kwargs):
 +        raise NotImplementedError("'update' not supported on frozendict")
 +
 +@contextmanager
 +def ignore(*exc):
 +    try:
 +        yield
 +    except exc:
 +        pass
 +
 +# Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
 +if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0'):
 +    def html_escape(text):
 +        return werkzeug.utils.escape(text, quote=True)
 +else:
 +    def html_escape(text):
 +        return werkzeug.utils.escape(text)
  
  # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@@ -39,6 -39,7 +39,6 @@@ from lxml import etre
  
  import config
  import misc
 -from misc import UpdateableStr
  from misc import SKIPPED_ELEMENT_TYPES
  import osutil
  import openerp
@@@ -49,15 -50,13 +49,15 @@@ _logger = logging.getLogger(__name__
  # used to notify web client that these translations should be loaded in the UI
  WEB_TRANSLATION_COMMENT = "openerp-web"
  
 +SKIPPED_ELEMENTS = ('script', 'style')
 +
  _LOCALE2WIN32 = {
      'af_ZA': 'Afrikaans_South Africa',
      'sq_AL': 'Albanian_Albania',
      'ar_SA': 'Arabic_Saudi Arabia',
      'eu_ES': 'Basque_Spain',
      'be_BY': 'Belarusian_Belarus',
-     'bs_BA': 'Serbian (Latin)',
+     'bs_BA': 'Bosnian_Bosnia and Herzegovina',
      'bg_BG': 'Bulgarian_Bulgaria',
      'ca_ES': 'Catalan_Spain',
      'hr_HR': 'Croatian_Croatia',
@@@ -166,55 -165,51 +166,55 @@@ class GettextAlias(object)
              return sql_db.db_connect(db_name)
  
      def _get_cr(self, frame, allow_create=True):
 -        is_new_cr = False
 -        cr = frame.f_locals.get('cr', frame.f_locals.get('cursor'))
 -        if not cr:
 -            s = frame.f_locals.get('self', {})
 -            cr = getattr(s, 'cr', None)
 -        if not cr and allow_create:
 +        # try, in order: cr, cursor, self.env.cr, self.cr
 +        if 'cr' in frame.f_locals:
 +            return frame.f_locals['cr'], False
 +        if 'cursor' in frame.f_locals:
 +            return frame.f_locals['cursor'], False
 +        s = frame.f_locals.get('self')
 +        if hasattr(s, 'env'):
 +            return s.env.cr, False
 +        if hasattr(s, 'cr'):
 +            return s.cr, False
 +        if allow_create:
 +            # create a new cursor
              db = self._get_db()
              if db is not None:
 -                cr = db.cursor()
 -                is_new_cr = True
 -        return cr, is_new_cr
 +                return db.cursor(), True
 +        return None, False
  
      def _get_uid(self, frame):
 -        return frame.f_locals.get('uid') or frame.f_locals.get('user')
 +        # try, in order: uid, user, self.env.uid
 +        if 'uid' in frame.f_locals:
 +            return frame.f_locals['uid']
 +        if 'user' in frame.f_locals:
 +            return int(frame.f_locals['user'])      # user may be a record
 +        s = frame.f_locals.get('self')
 +        return s.env.uid
  
      def _get_lang(self, frame):
 -        lang = None
 -        ctx = frame.f_locals.get('context')
 -        if not ctx:
 -            kwargs = frame.f_locals.get('kwargs')
 -            if kwargs is None:
 -                args = frame.f_locals.get('args')
 -                if args and isinstance(args, (list, tuple)) \
 -                        and isinstance(args[-1], dict):
 -                    ctx = args[-1]
 -            elif isinstance(kwargs, dict):
 -                ctx = kwargs.get('context')
 -        if ctx:
 -            lang = ctx.get('lang')
 -        s = frame.f_locals.get('self', {})
 -        if not lang:
 -            c = getattr(s, 'localcontext', None)
 -            if c:
 -                lang = c.get('lang')
 -        if not lang:
 -            # Last resort: attempt to guess the language of the user
 -            # Pitfall: some operations are performed in sudo mode, and we 
 -            #          don't know the originial uid, so the language may
 -            #          be wrong when the admin language differs.
 -            pool = getattr(s, 'pool', None)
 -            (cr, dummy) = self._get_cr(frame, allow_create=False)
 -            uid = self._get_uid(frame)
 -            if pool and cr and uid:
 -                lang = pool['res.users'].context_get(cr, uid)['lang']
 -        return lang
 +        # try, in order: context.get('lang'), kwargs['context'].get('lang'),
 +        # self.env.lang, self.localcontext.get('lang')
 +        if 'context' in frame.f_locals:
 +            return frame.f_locals['context'].get('lang')
 +        kwargs = frame.f_locals.get('kwargs', {})
 +        if 'context' in kwargs:
 +            return kwargs['context'].get('lang')
 +        s = frame.f_locals.get('self')
 +        if hasattr(s, 'env'):
 +            return s.env.lang
 +        if hasattr(s, 'localcontext'):
 +            return s.localcontext.get('lang')
 +        # Last resort: attempt to guess the language of the user
 +        # Pitfall: some operations are performed in sudo mode, and we
 +        #          don't know the originial uid, so the language may
 +        #          be wrong when the admin language differs.
 +        pool = getattr(s, 'pool', None)
 +        (cr, dummy) = self._get_cr(frame, allow_create=False)
 +        uid = self._get_uid(frame)
 +        if pool and cr and uid:
 +            return pool['res.users'].context_get(cr, uid)['lang']
 +        return None
  
      def __call__(self, source):
          res = source
@@@ -470,7 -465,7 +470,7 @@@ def trans_export(lang, modules, buffer
                  row.setdefault('tnrs', []).append((type, name, res_id))
                  row.setdefault('comments', set()).update(comments)
  
 -            for src, row in grouped_rows.items():
 +            for src, row in sorted(grouped_rows.items()):
                  if not lang:
                      # translation template, so no translation value
                      row['translation'] = ''
@@@ -545,34 -540,28 +545,34 @@@ def trans_parse_rml(de)
          res.extend(trans_parse_rml(n))
      return res
  
 -def trans_parse_view(de):
 -    res = []
 -    if not isinstance(de, SKIPPED_ELEMENT_TYPES) and de.text and de.text.strip():
 -        res.append(de.text.strip().encode("utf8"))
 -    if de.tail and de.tail.strip():
 -        res.append(de.tail.strip().encode("utf8"))
 -    if de.tag == 'attribute' and de.get("name") == 'string':
 -        if de.text:
 -            res.append(de.text.encode("utf8"))
 -    if de.get("string"):
 -        res.append(de.get('string').encode("utf8"))
 -    if de.get("help"):
 -        res.append(de.get('help').encode("utf8"))
 -    if de.get("sum"):
 -        res.append(de.get('sum').encode("utf8"))
 -    if de.get("confirm"):
 -        res.append(de.get('confirm').encode("utf8"))
 -    if de.get("placeholder"):
 -        res.append(de.get('placeholder').encode("utf8"))
 -    for n in de:
 -        res.extend(trans_parse_view(n))
 -    return res
 +def _push(callback, term, source_line):
 +    """ Sanity check before pushing translation terms """
 +    term = (term or "").strip().encode('utf8')
 +    # Avoid non-char tokens like ':' '...' '.00' etc.
 +    if len(term) > 8 or any(x.isalpha() for x in term):
 +        callback(term, source_line)
 +
 +def trans_parse_view(element, callback):
 +    """ Helper method to recursively walk an etree document representing a
 +        regular view and call ``callback(term)`` for each translatable term
 +        that is found in the document.
 +
 +        :param ElementTree element: root of etree document to extract terms from
 +        :param callable callback: a callable in the form ``f(term, source_line)``,
 +            that will be called for each extracted term.
 +    """
 +    if (not isinstance(element, SKIPPED_ELEMENT_TYPES)
 +            and element.tag.lower() not in SKIPPED_ELEMENTS
 +            and element.text):
 +        _push(callback, element.text, element.sourceline)
 +    if element.tail:
 +        _push(callback, element.tail, element.sourceline)
 +    for attr in ('string', 'help', 'sum', 'confirm', 'placeholder'):
 +        value = element.get(attr)
 +        if value:
 +            _push(callback, value, element.sourceline)
 +    for n in element:
 +        trans_parse_view(n, callback)
  
  # tests whether an object is in a list of modules
  def in_modules(object_name, modules):
      module = module_dict.get(module, module)
      return module in modules
  
 +def _extract_translatable_qweb_terms(element, callback):
 +    """ Helper method to walk an etree document representing
 +        a QWeb template, and call ``callback(term)`` for each
 +        translatable term that is found in the document.
 +
 +        :param ElementTree element: root of etree document to extract terms from
 +        :param callable callback: a callable in the form ``f(term, source_line)``,
 +            that will be called for each extracted term.
 +    """
 +    # not using elementTree.iterparse because we need to skip sub-trees in case
 +    # the ancestor element had a reason to be skipped
 +    for el in element:
 +        if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
 +        if (el.tag.lower() not in SKIPPED_ELEMENTS
 +                and "t-js" not in el.attrib
 +                and not ("t-jquery" in el.attrib and "t-operation" not in el.attrib)
 +                and not ("t-translation" in el.attrib and
 +                         el.attrib["t-translation"].strip() == "off")):
 +            _push(callback, el.text, el.sourceline)
 +            for att in ('title', 'alt', 'label', 'placeholder'):
 +                if att in el.attrib:
 +                    _push(callback, el.attrib[att], el.sourceline)
 +            _extract_translatable_qweb_terms(el, callback)
 +        _push(callback, el.tail, el.sourceline)
  
  def babel_extract_qweb(fileobj, keywords, comment_tags, options):
      """Babel message extractor for qweb template files.
      """
      result = []
      def handle_text(text, lineno):
 -        text = (text or "").strip()
 -        if len(text) > 1: # Avoid mono-char tokens like ':' ',' etc.
 -            result.append((lineno, None, text, []))
 -
 -    # not using elementTree.iterparse because we need to skip sub-trees in case
 -    # the ancestor element had a reason to be skipped
 -    def iter_elements(current_element):
 -        for el in current_element:
 -            if isinstance(el, SKIPPED_ELEMENT_TYPES): continue
 -            if "t-js" not in el.attrib and \
 -                    not ("t-jquery" in el.attrib and "t-operation" not in el.attrib) and \
 -                    not ("t-translation" in el.attrib and el.attrib["t-translation"].strip() == "off"):
 -                handle_text(el.text, el.sourceline)
 -                for att in ('title', 'alt', 'label', 'placeholder'):
 -                    if att in el.attrib:
 -                        handle_text(el.attrib[att], el.sourceline)
 -                iter_elements(el)
 -            handle_text(el.tail, el.sourceline)
 -
 +        result.append((lineno, None, text, []))
      tree = etree.parse(fileobj)
 -    iter_elements(tree.getroot())
 -
 +    _extract_translatable_qweb_terms(tree.getroot(), handle_text)
      return result
  
 -
  def trans_generate(lang, modules, cr):
      dbname = cr.dbname
  
          # empty and one-letter terms are ignored, they probably are not meant to be
          # translated, and would be very hard to translate anyway.
          if not source or len(source.strip()) <= 1:
 -            _logger.debug("Ignoring empty or 1-letter source term: %r", tuple)
              return
          if tuple not in _to_translate:
              _to_translate.append(tuple)
              return s.encode('utf8')
          return s
  
 +    def push(mod, type, name, res_id, term):
 +        term = (term or '').strip()
 +        if len(term) > 2:
 +            push_translation(mod, type, name, res_id, term)
 +
 +    def get_root_view(xml_id):
 +        view = model_data_obj.xmlid_to_object(cr, uid, xml_id)
 +        if view:
 +            while view.mode != 'primary':
 +                view = view.inherit_id
 +        xml_id = view.get_external_id(cr, uid).get(view.id, xml_id)
 +        return xml_id
 +
      for (xml_name,model,res_id,module) in cr.fetchall():
          module = encode(module)
          model = encode(model)
              _logger.error("Unable to find object %r", model)
              continue
  
 +        if not registry[model]._translate:
 +            # explicitly disabled
 +            continue
 +
          exists = registry[model].exists(cr, uid, res_id)
          if not exists:
              _logger.warning("Unable to find object %r with id %d", model, res_id)
  
          if model=='ir.ui.view':
              d = etree.XML(encode(obj.arch))
 -            for t in trans_parse_view(d):
 -                push_translation(module, 'view', encode(obj.model), 0, t)
 +            if obj.type == 'qweb':
 +                view_id = get_root_view(xml_name)
 +                push_qweb = lambda t,l: push(module, 'view', 'website', view_id, t)
 +                _extract_translatable_qweb_terms(d, push_qweb)
 +            else:
 +                push_view = lambda t,l: push(module, 'view', obj.model, xml_name, t)
 +                trans_parse_view(d, push_view)
          elif model=='ir.actions.wizard':
              pass # TODO Can model really be 'ir.actions.wizard' ?
  
                  _logger.error("name error in %s: %s", xml_name, str(exc))
                  continue
              objmodel = registry.get(obj.model)
 -            if not objmodel or not field_name in objmodel._columns:
 +            if (objmodel is None or field_name not in objmodel._columns
 +                    or not objmodel._translate):
                  continue
              field_def = objmodel._columns[field_name]
  
                  except (IOError, etree.XMLSyntaxError):
                      _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
  
 -        for field_name,field_def in obj._table._columns.items():
 +        for field_name, field_def in obj._columns.items():
 +            if model == 'ir.model' and field_name == 'name' and obj.name == obj.model:
 +                # ignore model name if it is the technical one, nothing to translate
 +                continue
              if field_def.translate:
                  name = model + "," + field_name
                  try:
 -                    trad = getattr(obj, field_name) or ''
 +                    term = obj[field_name] or ''
                  except:
 -                    trad = ''
 -                push_translation(module, 'model', name, xml_name, encode(trad))
 +                    term = ''
 +                push_translation(module, 'model', name, xml_name, encode(term))
  
          # End of data for ir.model.data query results
  
          if model_obj._sql_constraints:
              push_local_constraints(module, model_obj, 'sql_constraints')
  
 -    def get_module_paths():
 -        # default addons path (base)
 -        def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons'))
 -        mod_paths = set([ def_path ])
 -        ad_paths = map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(','))
 -        for adp in ad_paths:
 -            mod_paths.add(adp)
 -            if not os.path.isabs(adp):
 -                mod_paths.add(adp)
 -            elif adp != def_path and adp.startswith(def_path):
 -                mod_paths.add(adp[len(def_path)+1:])
 -        return list(mod_paths)
 -
 -    def get_module_from_path(path, mod_paths):
 -        for mp in mod_paths:
 -            if path.startswith(mp) and (os.path.dirname(path) != mp):
 -                path = path[len(mp)+1:]
 -                return path.split(os.path.sep)[0]
 -        return 'base'   # files that are not in a module are considered as being in 'base' module
 -
      modobj = registry['ir.module.module']
      installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')])
      installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name']))
  
 -    root_path = os.path.join(config.config['root_path'], 'addons')
 -
 -    apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(',')))
 -    if root_path in apaths:
 -        path_list = apaths
 -    else :
 -        path_list = [root_path,] + apaths
 -
 +    path_list = list(openerp.modules.module.ad_paths)
      # Also scan these non-addon paths
      for bin_path in ['osv', 'report' ]:
          path_list.append(os.path.join(config.config['root_path'], bin_path))
  
      _logger.debug("Scanning modules at paths: %s", path_list)
  
 -    mod_paths = get_module_paths()
 +    mod_paths = list(path_list)
 +
 +    def get_module_from_path(path):
 +        for mp in mod_paths:
 +            if path.startswith(mp) and (os.path.dirname(path) != mp):
 +                path = path[len(mp)+1:]
 +                return path.split(os.path.sep)[0]
 +        return 'base'   # files that are not in a module are considered as being in 'base' module
  
      def verified_module_filepaths(fname, path, root):
          fabsolutepath = join(root, fname)
          frelativepath = fabsolutepath[len(path):]
          display_path = "addons%s" % frelativepath
 -        module = get_module_from_path(fabsolutepath, mod_paths=mod_paths)
 +        module = get_module_from_path(fabsolutepath)
          if ('all' in modules or module in modules) and module in installed_modules:
              return module, fabsolutepath, frelativepath, display_path
          return None, None, None, None