[MERGE] forward port branch 11.0 up to bb6f6c57f9

This commit is contained in:
Christophe Simonis
2018-11-28 17:44:46 +01:00
34 changed files with 522 additions and 116 deletions
+28 -21
View File
@@ -438,15 +438,15 @@ class IrTranslation(models.Model):
if not callable(field.translate):
return
trans = self.env['ir.translation']
outdated = trans
discarded = trans
Translation = self.env['ir.translation']
outdated = Translation
discarded = Translation
for record in records:
# get field value and terms to translate
value = record[field.name]
terms = set(field.get_trans_terms(value))
record_trans = trans.search([
translations = Translation.search([
('type', '=', 'model'),
('name', '=', "%s,%s" % (field.model_name, field.name)),
('res_id', '=', record.id),
@@ -454,25 +454,32 @@ class IrTranslation(models.Model):
if not terms:
# discard all translations for that field
discarded += record_trans
discarded += translations
continue
# remap existing translations on terms when possible
trans_src = record_trans.mapped('src')
for trans in record_trans:
if trans.src == trans.value:
discarded += trans
elif trans.src not in terms:
matches = get_close_matches(trans.src, terms, 1, 0.9)
if matches:
if matches[0] in trans_src:
# there is already a translation for this term; discard this one
discarded += trans
else:
trans.write({'src': matches[0], 'state': trans.state})
trans_src.append(matches[0]) # avoid reuse of term
else:
outdated += trans
# remap existing translations on terms when possible; each term
# should be translated at most once per language
done = set() # {(src, lang), ...}
translations_to_match = []
for translation in translations:
if translation.src == translation.value:
discarded += translation
elif translation.src in terms:
done.add((translation.src, translation.lang))
else:
translations_to_match.append(translation)
for translation in translations_to_match:
matches = get_close_matches(translation.src, terms, 1, 0.9)
src = matches[0] if matches else None
if not src:
outdated += translation
elif (src, translation.lang) in done:
discarded += translation
else:
translation.write({'src': src, 'state': translation.state})
done.add((src, translation.lang))
# process outdated and discarded translations
outdated.write({'state': 'to_translate'})
+2
View File
@@ -448,6 +448,8 @@ class Users(models.Model):
@api.model
def check_credentials(self, password):
""" Override this method to plug additional authentication methods"""
if not password:
raise AccessDenied()
user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)])
if not user:
raise AccessDenied()
+50 -32
View File
@@ -310,30 +310,36 @@ class TestTranslation(TransactionCase):
class TestXMLTranslation(TransactionCase):
def setUp(self):
super(TestXMLTranslation, self).setUp()
self.env['ir.translation'].load_module_terms(['base'], ['fr_FR'])
self.env['ir.translation'].load_module_terms(['base'], ['fr_FR', 'nl_NL'])
def create_view(self, archf, terms, **kwargs):
view = self.env['ir.ui.view'].create({
'name': 'test',
'model': 'res.partner',
'arch': archf % terms,
})
for lang, trans_terms in kwargs.items():
for src, val in pycompat.izip(terms, trans_terms):
self.env['ir.translation'].create({
'type': 'model',
'name': 'ir.ui.view,arch_db',
'lang': lang,
'res_id': view.id,
'src': src,
'value': val,
'state': 'translated',
})
return view
def test_copy(self):
""" Create a simple view, fill in translations, and copy it. """
env_en = self.env(context={})
env_fr = self.env(context={'lang': 'fr_FR'})
archf = '<form string="%s"><div>%s</div><div>%s</div></form>'
terms_en = ('Knife', 'Fork', 'Spoon')
terms_fr = ('Couteau', 'Fourchette', 'Cuiller')
view0 = self.env['ir.ui.view'].create({
'name': 'test',
'model': 'res.partner',
'arch': archf % terms_en,
})
for src, value in list(pycompat.izip(terms_en, terms_fr)):
self.env['ir.translation'].create({
'type': 'model',
'name': 'ir.ui.view,arch_db',
'lang': 'fr_FR',
'res_id': view0.id,
'src': src,
'value': value,
})
view0 = self.create_view(archf, terms_en, fr_FR=terms_fr)
env_en = self.env(context={})
env_fr = self.env(context={'lang': 'fr_FR'})
# check translated field
self.assertEqual(view0.with_env(env_en).arch_db, archf % terms_en)
@@ -360,17 +366,29 @@ class TestXMLTranslation(TransactionCase):
archf = '<form string="%s"><div>%s</div><div>%s</div></form>'
terms_en = ('Knife', 'Fork', 'Spoon')
terms_fr = (' Couteau', 'Fourchette ', ' Cuiller ')
view0 = self.env['ir.ui.view'].create({
'name': 'test',
'model': 'res.partner',
'arch': archf % terms_en,
})
for src, value in list(pycompat.izip(terms_en, terms_fr)):
self.env['ir.translation'].create({
'type': 'model',
'name': 'ir.ui.view,arch_db',
'lang': 'fr_FR',
'res_id': view0.id,
'src': src,
'value': value,
})
self.create_view(archf, terms_en, fr_FR=terms_fr)
def test_sync(self):
""" Check translations after minor change in source terms. """
archf = '<form string="X">%s</form>'
terms_en = ('Bread and cheeze',)
terms_fr = ('Pain et fromage',)
terms_nl = ('Brood and kaas',)
view = self.create_view(archf, terms_en, fr_FR=terms_fr, nl_NL=terms_nl)
env_en = self.env(context={})
env_fr = self.env(context={'lang': 'fr_FR'})
env_nl = self.env(context={'lang': 'nl_NL'})
self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr)
self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl)
# modify source term in view (fixed type in 'cheeze')
terms_en = ('Bread and cheese',)
view.write({'arch_db': archf % terms_en})
# check whether translations have been synchronized
self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr)
self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl)
@@ -1,4 +1,8 @@
# -*- coding: utf-8 -*-
try:
from unittest.mock import patch
except ImportError:
from mock import patch
from odoo.tests import common
@@ -465,3 +469,32 @@ class TestOnChange(common.TransactionCase):
result = Message.onchange(value, ['message', 'message_name', 'message_currency'], field_onchange)
self.assertEqual(result['value'], onchange_result)
def test_onchange_many2one_one2many(self):
""" Setting a many2one field should not read the inverse one2many. """
discussion = self.env.ref('test_new_api.discussion_0')
field_onchange = self.Message._onchange_spec()
self.assertEqual(field_onchange.get('discussion'), '1')
values = {
'discussion': discussion.id,
'name': "[%s] %s" % ('', self.env.user.name),
'body': False,
'author': self.env.uid,
'size': 0,
}
called = [False]
orig_read = type(discussion).read
def mock_read(self, fields=None, load='_classic_read'):
if discussion in self and 'messages' in (fields or ()):
called[0] = True
return orig_read(self, fields, load)
# changing 'discussion' on message should not read 'messages' on discussion
with patch.object(type(discussion), 'read', mock_read, create=True):
self.env.cache.invalidate()
self.Message.onchange(values, 'discussion', field_onchange)
self.assertFalse(called[0], "discussion.messages has been read")
+6
View File
@@ -983,6 +983,12 @@ class Cache(object):
value = self._data[field][record.id].get(key, SpecialValue(None))
return default if isinstance(value, SpecialValue) else value
def get_special(self, record, field, default=None):
""" Return the special value of ``field`` for ``record``. """
key = field.cache_key(record)
value = self._data[field][record.id].get(key)
return value.get if isinstance(value, SpecialValue) else default
def set_special(self, record, field, getter):
""" Set the value of ``field`` for ``record`` to return ``getter()``. """
key = field.cache_key(record)
+27 -12
View File
@@ -2054,6 +2054,26 @@ class Many2one(_Relational):
return super(Many2one, self).convert_to_onchange(value, record, names)
class _RelationalMultiUpdate(object):
""" A getter to update the value of an x2many field, without reading its
value until necessary.
"""
__slots__ = ['record', 'field', 'value']
def __init__(self, record, field, value):
self.record = record
self.field = field
self.value = value
def __call__(self):
# determine the current field's value, and update it in cache only
record, field, value = self.record, self.field, self.value
cache = record.env.cache
cache.remove(record, field)
val = field.convert_to_cache(record[field.name] | value, record, validate=False)
cache.set(record, field, val)
return val
class _RelationalMulti(_Relational):
""" Abstract class for relational fields *2many. """
@@ -2065,21 +2085,16 @@ class _RelationalMulti(_Relational):
""" Update the cached value of ``self`` for ``records`` with ``value``. """
cache = records.env.cache
for record in records:
if cache.contains(record, self):
special = cache.get_special(record, self)
if isinstance(special, _RelationalMultiUpdate):
# include 'value' in the existing _RelationalMultiUpdate; this
# avoids reading the field's value (which may be large)
special.value |= value
elif cache.contains(record, self):
val = self.convert_to_cache(record[self.name] | value, record, validate=False)
cache.set(record, self, val)
else:
cache.set_special(record, self, self._update_getter(record, value))
def _update_getter(self, record, value):
def getter():
# determine the current field's value, and update it in cache only
cache = record.env.cache
cache.remove(record, self)
val = self.convert_to_cache(record[self.name] | value, record, validate=False)
cache.set(record, self, val)
return val
return getter
cache.set_special(record, self, _RelationalMultiUpdate(record, self, value))
def convert_to_cache(self, value, record, validate=True):
# cache format: tuple(ids)