[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
This commit is contained in:
Christophe Simonis 2013-08-21 12:31:39 +02:00
commit 208b697239
13 changed files with 127 additions and 45 deletions

View File

@ -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 ``\``)
- 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``.

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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 _get_current_rate(self, cr, uid, ids, name, arg, context=None):
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, 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)),

View File

@ -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>

View File

@ -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

View File

@ -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))
# --------------------------------------------------

View File

@ -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)

View File

@ -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:

View File

@ -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()

View File

@ -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)
# 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 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)
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

View File

@ -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',