#
# 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
from functools import wraps
import cProfile
+from contextlib import contextmanager
import subprocess
import logging
import os
import sys
import threading
import time
+import werkzeug.utils
import zipfile
from collections import defaultdict, Mapping
from datetime import datetime
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.
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)
"""
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)
'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',
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:
import config
import misc
-from misc import UpdateableStr
from misc import SKIPPED_ELEMENT_TYPES
import osutil
import openerp
# 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',
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
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'] = ''
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