summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@openerp.com>2013-08-21 12:31:39 +0200
committerChristophe Simonis <chs@openerp.com>2013-08-21 12:31:39 +0200
commit208b69723986e79db5a8893cf8540f75720b63bc (patch)
tree7476f251a6a049da1fc2641bcd414dc388382044
parentc02afb45fa74fb1490b4b4965f987906ee718eb6 (diff)
parent8179683143f880b10f40559a3da48afcfcfcd8ee (diff)
[MERGE] forward port of branch saas-1 up to revid 4899 chs@openerp.com-20130821094758-1ae0d1ml5obufzxv
bzr revid: odo@openerp.com-20130820160346-qcdy4kavgm5cb9t6 bzr revid: chs@openerp.com-20130821103139-zqqbrt8dq6iixrny
-rw-r--r--doc/changelog.rst14
-rw-r--r--openerp/addons/base/ir/ir_model.py3
-rw-r--r--openerp/addons/base/ir/ir_translation.py12
-rw-r--r--openerp/addons/base/ir/ir_ui_view.py2
-rw-r--r--openerp/addons/base/res/res_currency.py15
-rw-r--r--openerp/addons/base/res/res_currency_view.xml4
-rw-r--r--openerp/addons/base/res/res_partner.py2
-rw-r--r--openerp/osv/expression.py4
-rw-r--r--openerp/osv/orm.py8
-rw-r--r--openerp/report/report_sxw.py8
-rw-r--r--openerp/tests/test_acl.py49
-rw-r--r--openerp/tools/mail.py50
-rw-r--r--openerp/tools/misc.py1
13 files changed, 127 insertions, 45 deletions
diff --git a/doc/changelog.rst b/doc/changelog.rst
index ce639d2d3c8..eadfe81b5e8 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -31,5 +31,15 @@ Changelog
``openerp.exceptions.RedirectWarning``.
- Give a pair of new methods to ``res.config.settings`` and a helper to make
them easier to use: ``get_config_warning()``.
-- Path to webkit report files (field ``report_file``) must be writen with the
- Unix way (with ``/`` and not ``\``) \ No newline at end of file
+- Path to webkit report files (field ``report_file``) must be written the
+ Unix way (with ``/`` and not ``\``)
+
+
+`7.0`
+-----
+
+- Modules may now include an ``i18n_extra`` directory that will be treated like the
+ default ``i18n`` directory. This is typically useful for manual translation files
+ that are not managed by Launchpad's translation system. An example is l10n modules
+ that depend on ``l10n_multilang``.
+
diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py
index 7eb4f5ecd94..3b709934a27 100644
--- a/openerp/addons/base/ir/ir_model.py
+++ b/openerp/addons/base/ir/ir_model.py
@@ -195,7 +195,8 @@ class ir_model(osv.osv):
ctx = dict(context,
field_name=vals['name'],
field_state='manual',
- select=vals.get('select_level', '0'))
+ select=vals.get('select_level', '0'),
+ update_custom_fields=True)
self.pool[vals['model']]._auto_init(cr, ctx)
openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
return res
diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py
index acb6daa7785..0f9b6ea3886 100644
--- a/openerp/addons/base/ir/ir_translation.py
+++ b/openerp/addons/base/ir/ir_translation.py
@@ -445,6 +445,13 @@ class ir_translation(osv.osv):
tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
+ # i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
+ base_trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
+ if base_trans_extra_file:
+ _logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
+ tools.trans_load(cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
+ context['overwrite'] = True # make sure the requested translation will override the base terms later
+
# Step 2: then load the main translation file, possibly overriding the terms coming from the base language
trans_file = openerp.modules.get_module_resource(module_name, 'i18n', lang_code + '.po')
if trans_file:
@@ -452,6 +459,11 @@ class ir_translation(osv.osv):
tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
elif lang_code != 'en_US':
_logger.warning('module %s: no translation for language %s', module_name, lang_code)
+
+ trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
+ if trans_extra_file:
+ _logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
+ tools.trans_load(cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
return True
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 73530a122ad..9699545ddf5 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -187,7 +187,7 @@ class view(osv.osv):
if self.pool._init:
# Module init currently in progress, only consider views from modules whose code was already loaded
query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
- WHERE v.inherit_id=%s AND v.model=%s AND md.module in %s
+ WHERE v.inherit_id=%s AND v.model=%s AND (md.module IS NULL or md.module in %s)
ORDER BY priority"""
query_params = (view_id, model, tuple(self.pool._init_modules))
else:
diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py
index efe33f32fd9..a496a1eccdf 100644
--- a/openerp/addons/base/res/res_currency.py
+++ b/openerp/addons/base/res/res_currency.py
@@ -30,11 +30,13 @@ from openerp.tools.translate import _
CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?')
class res_currency(osv.osv):
-
def _current_rate(self, cr, uid, ids, name, arg, context=None):
- return self._get_current_rate(cr, uid, ids, name, arg, context=context)
+ return self._get_current_rate(cr, uid, ids, context=context)
+
+ def _current_rate_silent(self, cr, uid, ids, name, arg, context=None):
+ return self._get_current_rate(cr, uid, ids, raise_on_no_rate=False, context=context)
- def _get_current_rate(self, cr, uid, ids, name, arg, context=None):
+ def _get_current_rate(self, cr, uid, ids, raise_on_no_rate=True, context=None):
if context is None:
context = {}
res = {}
@@ -52,9 +54,12 @@ class res_currency(osv.osv):
if cr.rowcount:
id, rate = cr.fetchall()[0]
res[id] = rate
+ elif not raise_on_no_rate:
+ res[id] = 0
else:
raise osv.except_osv(_('Error!'),_("No currency rate associated for currency %d for the given period" % (id)))
return res
+
_name = "res.currency"
_description = "Currency"
_columns = {
@@ -63,6 +68,10 @@ class res_currency(osv.osv):
'symbol': fields.char('Symbol', size=4, help="Currency sign, to be used when printing amounts."),
'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6),
help='The rate of the currency to the currency of rate 1.'),
+
+ # Do not use for computation ! Same as rate field with silent failing
+ 'rate_silent': fields.function(_current_rate_silent, string='Current Rate', digits=(12,6),
+ help='The rate of the currency to the currency of rate 1 (0 if no rate defined).'),
'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'),
'accuracy': fields.integer('Computational Accuracy'),
'rounding': fields.float('Rounding Factor', digits=(12,6)),
diff --git a/openerp/addons/base/res/res_currency_view.xml b/openerp/addons/base/res/res_currency_view.xml
index 907eec866fb..c74b677ea72 100644
--- a/openerp/addons/base/res/res_currency_view.xml
+++ b/openerp/addons/base/res/res_currency_view.xml
@@ -22,7 +22,7 @@
<field name="company_id" groups="base.group_multi_company"/>
<field name="rate_ids" invisible="1"/>
<field name="date"/>
- <field name="rate"/>
+ <field name="rate_silent"/>
<field name="rounding"/>
<field name="accuracy"/>
<field name="position"/>
@@ -37,7 +37,7 @@
<form string="Currency" version="7.0">
<group col="4">
<field name="name"/>
- <field name="rate"/>
+ <field name="rate_silent"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py
index 566ee6066dd..97b0ac454ae 100644
--- a/openerp/addons/base/res/res_partner.py
+++ b/openerp/addons/base/res/res_partner.py
@@ -208,6 +208,8 @@ class res_partner(osv.osv, format_address):
return result
def _display_name_compute(self, cr, uid, ids, name, args, context=None):
+ context = dict(context or {})
+ context.pop('show_address', None)
return dict(self.name_get(cr, uid, ids, context=context))
# indirections to avoid passing a copy of the overridable method when declaring the function field
diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py
index ef02882422c..18a1e9508f5 100644
--- a/openerp/osv/expression.py
+++ b/openerp/osv/expression.py
@@ -398,7 +398,9 @@ def is_leaf(element, internal=False):
INTERNAL_OPS += ('inselect',)
return (isinstance(element, tuple) or isinstance(element, list)) \
and len(element) == 3 \
- and element[1] in INTERNAL_OPS
+ and element[1] in INTERNAL_OPS \
+ and ((isinstance(element[0], basestring) and element[0])
+ or element in (TRUE_LEAF, FALSE_LEAF))
# --------------------------------------------------
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index 163abcb006d..f96d00815c9 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -383,16 +383,18 @@ class browse_record(object):
raise KeyError(error_msg)
# if the field is a classic one or a many2one, we'll fetch all classic and many2one fields
- if col._prefetch:
+ if col._prefetch and not col.groups:
# gen the list of "local" (ie not inherited) fields which are classic or many2one
- fields_to_fetch = filter(lambda x: x[1]._classic_write and x[1]._prefetch, self._table._columns.items())
+ field_filter = lambda x: x[1]._classic_write and x[1]._prefetch and not x[1].groups
+ fields_to_fetch = filter(field_filter, self._table._columns.items())
# gen the list of inherited fields
inherits = map(lambda x: (x[0], x[1][2]), self._table._inherit_fields.items())
# complete the field list with the inherited fields which are classic or many2one
- fields_to_fetch += filter(lambda x: x[1]._classic_write and x[1]._prefetch, inherits)
+ fields_to_fetch += filter(field_filter, inherits)
# otherwise we fetch only that field
else:
fields_to_fetch = [(name, col)]
+
ids = filter(lambda id: name not in self._data[id], self._data.keys())
# read the results
field_names = map(lambda x: x[0], fields_to_fetch)
diff --git a/openerp/report/report_sxw.py b/openerp/report/report_sxw.py
index fceb318401b..548a3f833f5 100644
--- a/openerp/report/report_sxw.py
+++ b/openerp/report/report_sxw.py
@@ -109,7 +109,7 @@ class _date_format(str, _format):
if self.val:
if getattr(self,'name', None):
date = datetime.strptime(self.name[:get_date_length()], DEFAULT_SERVER_DATE_FORMAT)
- return date.strftime(str(self.lang_obj.date_format))
+ return date.strftime(self.lang_obj.date_format.encode('utf-8'))
return self.val
class _dttime_format(str, _format):
@@ -120,8 +120,8 @@ class _dttime_format(str, _format):
def __str__(self):
if self.val and getattr(self,'name', None):
return datetime.strptime(self.name, DEFAULT_SERVER_DATETIME_FORMAT)\
- .strftime("%s %s"%(str(self.lang_obj.date_format),
- str(self.lang_obj.time_format)))
+ .strftime("%s %s"%((self.lang_obj.date_format).encode('utf-8'),
+ (self.lang_obj.time_format).encode('utf-8')))
return self.val
@@ -313,7 +313,7 @@ class rml_parse(object):
date = datetime_field.context_timestamp(self.cr, self.uid,
timestamp=date,
context=self.localcontext)
- return date.strftime(date_format)
+ return date.strftime(date_format.encode('utf-8'))
res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
if currency_obj:
diff --git a/openerp/tests/test_acl.py b/openerp/tests/test_acl.py
index 1d8d6bfb22a..e80390cd5a2 100644
--- a/openerp/tests/test_acl.py
+++ b/openerp/tests/test_acl.py
@@ -9,6 +9,7 @@ import common
# test group that demo user should not have
GROUP_TECHNICAL_FEATURES = 'base.group_no_one'
+
class TestACL(common.TransactionCase):
def setUp(self):
@@ -22,25 +23,25 @@ class TestACL(common.TransactionCase):
def test_field_visibility_restriction(self):
"""Check that model-level ``groups`` parameter effectively restricts access to that
- field for users who do not belong to one of the explicitly allowed groups"""
+ field for users who do not belong to one of the explicitly allowed groups"""
# Verify the test environment first
original_fields = self.res_currency.fields_get(self.cr, self.demo_uid, [])
form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form')
view_arch = etree.fromstring(form_view.get('arch'))
has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES)
self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group before the test")
- self.assertTrue('rate' in original_fields, "'rate' field must be properly visible before the test")
- self.assertNotEquals(view_arch.xpath("//field[@name='rate']"), [],
- "Field 'rate' must be found in view definition before the test")
+ self.assertTrue('accuracy' in original_fields, "'accuracy' field must be properly visible before the test")
+ self.assertNotEquals(view_arch.xpath("//field[@name='accuracy']"), [],
+ "Field 'accuracy' must be found in view definition before the test")
# Restrict access to the field and check it's gone
- self.res_currency._columns['rate'].groups = GROUP_TECHNICAL_FEATURES
+ self.res_currency._columns['accuracy'].groups = GROUP_TECHNICAL_FEATURES
fields = self.res_currency.fields_get(self.cr, self.demo_uid, [])
form_view = self.res_currency.fields_view_get(self.cr, self.demo_uid, False, 'form')
view_arch = etree.fromstring(form_view.get('arch'))
- self.assertFalse('rate' in fields, "'rate' field should be gone")
- self.assertEquals(view_arch.xpath("//field[@name='rate']"), [],
- "Field 'rate' must not be found in view definition")
+ self.assertFalse('accuracy' in fields, "'accuracy' field should be gone")
+ self.assertEquals(view_arch.xpath("//field[@name='accuracy']"), [],
+ "Field 'accuracy' must not be found in view definition")
# Make demo user a member of the restricted group and check that the field is back
self.tech_group.write({'users': [(4, self.demo_uid)]})
@@ -50,13 +51,13 @@ class TestACL(common.TransactionCase):
view_arch = etree.fromstring(form_view.get('arch'))
#import pprint; pprint.pprint(fields); pprint.pprint(form_view)
self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group")
- self.assertTrue('rate' in fields, "'rate' field must be properly visible again")
- self.assertNotEquals(view_arch.xpath("//field[@name='rate']"), [],
- "Field 'rate' must be found in view definition again")
+ self.assertTrue('accuracy' in fields, "'accuracy' field must be properly visible again")
+ self.assertNotEquals(view_arch.xpath("//field[@name='accuracy']"), [],
+ "Field 'accuracy' must be found in view definition again")
#cleanup
self.tech_group.write({'users': [(3, self.demo_uid)]})
- self.res_currency._columns['rate'].groups = False
+ self.res_currency._columns['accuracy'].groups = False
@mute_logger('openerp.osv.orm')
def test_field_crud_restriction(self):
@@ -65,7 +66,7 @@ class TestACL(common.TransactionCase):
has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES)
self.assertFalse(has_tech_feat, "`demo` user should not belong to the restricted group")
self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids']))
- self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []}))
+ self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []}))
# Now restrict access to the field and check it's forbidden
self.res_partner._columns['bank_ids'].groups = GROUP_TECHNICAL_FEATURES
@@ -79,12 +80,30 @@ class TestACL(common.TransactionCase):
has_tech_feat = self.res_users.has_group(self.cr, self.demo_uid, GROUP_TECHNICAL_FEATURES)
self.assertTrue(has_tech_feat, "`demo` user should now belong to the restricted group")
self.assert_(self.res_partner.read(self.cr, self.demo_uid, [1], ['bank_ids']))
- self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []}))
-
+ self.assert_(self.res_partner.write(self.cr, self.demo_uid, [1], {'bank_ids': []}))
+
#cleanup
self.tech_group.write({'users': [(3, self.demo_uid)]})
self.res_partner._columns['bank_ids'].groups = False
+ def test_fields_browse_restriction(self):
+ """Test access to records having restricted fields"""
+ self.res_partner._columns['email'].groups = GROUP_TECHNICAL_FEATURES
+ try:
+ P = self.res_partner
+ pid = P.search(self.cr, self.demo_uid, [], limit=1)[0]
+ part = P.browse(self.cr, self.demo_uid, pid)
+ # accessing fields must no raise exceptions...
+ part.name
+ # ... except they are restricted
+ with self.assertRaises(openerp.osv.orm.except_orm) as cm:
+ part.email
+
+ self.assertEqual(cm.exception.args[0], 'Access Denied')
+ finally:
+ self.res_partner._columns['email'].groups = False
+
+
if __name__ == '__main__':
unittest2.main()
diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py
index 6ab858d1d67..4581d69c6ef 100644
--- a/openerp/tools/mail.py
+++ b/openerp/tools/mail.py
@@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
-# Copyright (C) 2012 OpenERP S.A. (<http://openerp.com>).
+# Copyright (C) 2012-2013 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
@@ -44,29 +44,53 @@ tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "if
tags_to_remove = ['html', 'body', 'font']
-def html_sanitize(src):
+def html_sanitize(src, silent=True):
if not src:
return src
src = ustr(src, errors='replace')
+ logger = _logger.getChild('html_sanitize')
+
# html encode email tags
part = re.compile(r"(<(([^a<>]|a[^<>\s])[^<>]*)@[^<>]+>)", re.IGNORECASE | re.DOTALL)
src = part.sub(lambda m: cgi.escape(m.group(1)), src)
- # some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
+ kwargs = {
+ 'page_structure': True,
+ 'style': False, # do not remove style attributes
+ 'forms': True, # remove form tags
+ }
+ if etree.LXML_VERSION >= (2, 3, 1):
+ # kill_tags attribute has been added in version 2.3.1
+ kwargs.update({
+ 'kill_tags': tags_to_kill,
+ 'remove_tags': tags_to_remove,
+ })
+ else:
+ kwargs['remove_tags'] = tags_to_kill + tags_to_remove
+
+ if etree.LXML_VERSION >= (3, 1, 0):
+ kwargs.update({
+ 'safe_attrs_only': True,
+ 'safe_attrs': clean.defs.safe_attrs | set(['style']),
+ })
+ else:
+ # lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
+ kwargs['safe_attrs_only'] = False
+
try:
- cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, kill_tags=tags_to_kill, remove_tags=tags_to_remove)
- cleaned = cleaner.clean_html(src)
- except TypeError, e:
- # lxml.clean version < 2.3.1 does not have a kill_tags attribute
- # to remove in 2014
- cleaner = clean.Cleaner(page_structure=True, style=False, safe_attrs_only=False, forms=False, remove_tags=tags_to_kill + tags_to_remove)
+ # some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
+ cleaner = clean.Cleaner(**kwargs)
cleaned = cleaner.clean_html(src)
- except etree.ParserError, e:
- _logger.warning('html_sanitize: ParserError "%s" obtained when sanitizing "%s"' % (e, src))
+ except etree.ParserError:
+ if not silent:
+ raise
+ logger.warning('ParserError obtained when sanitizing %r', src, exc_info=True)
cleaned = '<p>ParserError when sanitizing</p>'
- except Exception, e:
- _logger.warning('html_sanitize: unknown error "%s" obtained when sanitizing "%s"' % (e, src))
+ except Exception:
+ if not silent:
+ raise
+ logger.warning('unknown error obtained when sanitizing %r', src, exc_info=True)
cleaned = '<p>Unknown error when sanitizing</p>'
return cleaned
diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py
index 4f2d8177c0f..65e7c38e536 100644
--- a/openerp/tools/misc.py
+++ b/openerp/tools/misc.py
@@ -421,6 +421,7 @@ def get_iso_codes(lang):
ALL_LANGUAGES = {
'ab_RU': u'Abkhazian / аҧсуа',
+ 'am_ET': u'Amharic / አምሃርኛ',
'ar_SY': u'Arabic / الْعَرَبيّة',
'bg_BG': u'Bulgarian / български език',
'bs_BS': u'Bosnian / bosanski jezik',