changeset 222402492614 in trytond:5.6
details: https://hg.tryton.org/trytond?cmd=changeset&node=222402492614
description:
        Ensure to join with only one translation row

        If there are multiple translations for the same record, the join will 
generate
        result with multiple times the same id on ModelSQL.search.
        As DISTINCT ON is not always supported, we must also ensure that 
duplicated
        translations are all updated.

        issue10061
        review351261002
        (grafted from 8a229271cc541c5ea16971d31f98314412622c32)
diffstat:

 trytond/ir/translation.py      |  43 ++++++++++++++++--------------
 trytond/model/fields/field.py  |  59 ++++++++++++++++++++++++-----------------
 trytond/tests/test_modelsql.py |  58 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 116 insertions(+), 44 deletions(-)

diffs (289 lines):

diff -r 7179827c1c12 -r 222402492614 trytond/ir/translation.py
--- a/trytond/ir/translation.py Wed Mar 03 18:11:23 2021 +0100
+++ b/trytond/ir/translation.py Sat Feb 20 00:48:50 2021 +0100
@@ -410,32 +410,34 @@
                     return record.model + ',' + field_name
 
             with Transaction().set_context(_check_access=False):
-                translations = {}
+                name2translations = defaultdict(list)
                 for translation in cls.search([
                             ('lang', '=', lang),
                             ('type', '=', ttype),
                             ('name', 'in', [get_name(r) for r in records]),
                             ]):
-                    translations[translation.name] = translation
+                    name2translations[translation.name].append(translation)
 
                 to_save = []
                 for record, value in zip(records, values):
-                    translation = translations.get(get_name(record))
+                    translations = name2translations.get(get_name(record))
                     if lang == 'en':
                         src = value
                     else:
                         src = getattr(record, field_name)
-                    if not translation:
+                    if not translations:
                         if not src and not value:
                             continue
                         translation = cls()
                         translation.name = name
                         translation.lang = lang
                         translation.type = ttype
-                    translation.src = src
-                    translation.value = value
-                    translation.fuzzy = False
-                    to_save.append(translation)
+                        translations.append(translation)
+                    for translation in translations:
+                        translation.src = src
+                        translation.value = value
+                        translation.fuzzy = False
+                        to_save.append(translation)
                 cls.save(to_save)
             return
 
@@ -443,7 +445,8 @@
         with Transaction().set_context(language=Config.get_language()):
             records = Model.browse(ids)
 
-        translations = {}
+        id2translations = defaultdict(list)
+        other_translations = defaultdict(list)
         with Transaction().set_context(_check_access=False):
             for translation in cls.search([
                         ('lang', '=', lang),
@@ -451,9 +454,8 @@
                         ('name', '=', name),
                         ('res_id', 'in', ids),
                         ]):
-                translations[translation.res_id] = translation
+                id2translations[translation.res_id].append(translation)
 
-            other_translations = {}
             if (lang == Config.get_language()
                     and Transaction().context.get('fuzzy_translation', True)):
                 for translation in cls.search([
@@ -462,17 +464,16 @@
                             ('name', '=', name),
                             ('res_id', 'in', ids),
                             ]):
-                    other_translations.setdefault(translation.res_id, []
-                        ).append(translation)
+                    other_translations[translation.res_id].append(translation)
 
             to_save = []
             for record, value in zip(records, values):
-                translation = translations.get(record.id)
+                translations = id2translations[record.id]
                 if lang == Config.get_language():
                     src = value
                 else:
                     src = getattr(record, field_name)
-                if not translation:
+                if not translations:
                     if not src and not value:
                         continue
                     translation = cls()
@@ -480,17 +481,19 @@
                     translation.lang = lang
                     translation.type = ttype
                     translation.res_id = record.id
+                    translations.append(translation)
                 else:
-                    other_langs = other_translations.get(record.id)
+                    other_langs = other_translations[record.id]
                     if other_langs:
                         for other_lang in other_langs:
                             other_lang.src = src
                             other_lang.fuzzy = True
                             to_save.append(other_lang)
-                translation.value = value
-                translation.src = src
-                translation.fuzzy = False
-                to_save.append(translation)
+                for translation in translations:
+                    translation.value = value
+                    translation.src = src
+                    translation.fuzzy = False
+                    to_save.append(translation)
             cls.save(to_save)
 
     @classmethod
diff -r 7179827c1c12 -r 222402492614 trytond/model/fields/field.py
--- a/trytond/model/fields/field.py     Wed Mar 03 18:11:23 2021 +0100
+++ b/trytond/model/fields/field.py     Sat Feb 20 00:48:50 2021 +0100
@@ -3,11 +3,14 @@
 import warnings
 from functools import wraps
 
+import sql
 from sql import (operators, Column, Literal, Select, CombiningQuery, Null,
     Query, Expression, Cast)
+from sql.aggregate import Min
 from sql.conditionals import Coalesce, NullIf
 from sql.operators import Concat
 
+from trytond import backend
 from trytond.pyson import PYSON, PYSONEncoder, PYSONDecoder, Eval
 from trytond.const import OPERATORS
 from trytond.tools.string_ import StringPartitioned, LazyString
@@ -17,6 +20,8 @@
 
 from ...rpc import RPC
 
+_sql_version = tuple(map(int, sql.__version__.split('.')))
+
 
 def domain_validate(value):
     assert isinstance(value, list), 'domain must be a list'
@@ -492,34 +497,40 @@
     def _get_translation_join(self, Model, name,
             translation, model, table, from_, language):
         if Model.__name__ == 'ir.model':
-            return from_.join(translation, 'LEFT',
-                condition=(translation.name == Concat(Concat(
-                            table.model, ','), name))
-                & (translation.res_id == -1)
-                & (translation.lang == language)
-                & (translation.type == 'model')
-                & (translation.fuzzy == Literal(False)))
+            name_ = Concat(Concat(table.model, ','), name)
+            type_ = 'model'
+            res_id = -1
         elif Model.__name__ == 'ir.model.field':
+            name_ = Concat(Concat(model.model, ','), table.name)
             if name == 'field_description':
                 type_ = 'field'
             else:
                 type_ = 'help'
-            return from_.join(model, 'LEFT',
-                condition=model.id == table.model).join(
-                    translation, 'LEFT',
-                    condition=(translation.name == Concat(Concat(
-                                model.model, ','), table.name))
-                    & (translation.res_id == -1)
-                    & (translation.lang == language)
-                    & (translation.type == type_)
-                    & (translation.fuzzy == Literal(False)))
+            res_id = -1
+        else:
+            name_ = '%s,%s' % (Model.__name__, name)
+            type_ = 'model'
+            res_id = table.id
+        if backend.name == 'postgresql' and _sql_version >= (1, 1, 0):
+            query = translation.select(
+                translation.res_id.as_('res_id'),
+                translation.value.as_('value'),
+                distinct=True,
+                distinct_on=[translation.res_id],
+                order_by=[translation.res_id, translation.id.desc])
         else:
-            return from_.join(translation, 'LEFT',
-                condition=(translation.res_id == table.id)
-                & (translation.name == '%s,%s' % (Model.__name__, name))
-                & (translation.lang == language)
-                & (translation.type == 'model')
-                & (translation.fuzzy == Literal(False)))
+            query = translation.select(
+                translation.res_id.as_('res_id'),
+                Min(translation.value).as_('value'),
+                group_by=[translation.res_id])
+        query.where = (
+            (translation.lang == language)
+            & (translation.type == type_)
+            & (translation.name == name_)
+            & (translation.fuzzy == Literal(False))
+            )
+        return query, from_.join(query, 'LEFT',
+            condition=(query.res_id == res_id))
 
     def convert_domain(self, domain, tables, Model):
         from trytond.ir.lang import get_parent_language
@@ -537,7 +548,7 @@
         column = None
         while language:
             translation = Translation.__table__()
-            join = self._get_translation_join(
+            translation, join = self._get_translation_join(
                 Model, name, translation, model, table, join, language)
             column = Coalesce(NullIf(column, ''), translation.value)
             language = get_parent_language(language)
@@ -573,7 +584,7 @@
             if key not in tables:
                 translation = Translation.__table__()
                 model = IrModel.__table__()
-                join = self._get_translation_join(
+                translation, join = self._get_translation_join(
                     Model, name, translation, model, table, table, language)
                 if join.left == table:
                     tables[key] = {
diff -r 7179827c1c12 -r 222402492614 trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.py    Wed Mar 03 18:11:23 2021 +0100
+++ b/trytond/tests/test_modelsql.py    Sat Feb 20 00:48:50 2021 +0100
@@ -768,6 +768,64 @@
         self.assertEqual(record.name, "Foo")
         self.assertEqual(other.name, "Baz")
 
+    @with_transaction()
+    def test_search_unique_result(self):
+        "Test unique result on search"
+        pool = Pool()
+        Model = pool.get('test.modelsql.translation')
+        Translation = pool.get('ir.translation')
+
+        with Transaction().set_context(language=self.default_language):
+            record, = Model.create([{'name': "Foo"}])
+        with Transaction().set_context(language=self.other_language):
+            Model.write([record], {'name': "Bar"})
+
+        translation, = Translation.search([
+                ('lang', '=', self.other_language),
+                ('name', '=', 'test.modelsql.translation,name'),
+                ('type', '=', 'model'),
+                ('res_id', '=', record.id),
+                ])
+        Translation.copy([translation], default={'value': "Baz"})
+
+        with Transaction().set_context(language=self.other_language):
+            self.assertEqual(
+                Model.search([('name', 'like', 'Ba%')]),
+                [record])
+            self.assertEqual(
+                Model.search([], order=[('name', 'DESC')]),
+                [record])
+
+    @unittest.skipIf(backend.name != 'postgresql',
+        "Only PostgreSQL support DISTINCT ON")
+    @with_transaction()
+    def test_search_last_translation(self):
+        "Test unique result on search"
+        pool = Pool()
+        Model = pool.get('test.modelsql.translation')
+        Translation = pool.get('ir.translation')
+
+        with Transaction().set_context(language=self.default_language):
+            record, = Model.create([{'name': "Foo"}])
+        with Transaction().set_context(language=self.other_language):
+            Model.write([record], {'name': "Bar"})
+
+        translation, = Translation.search([
+                ('lang', '=', self.other_language),
+                ('name', '=', 'test.modelsql.translation,name'),
+                ('type', '=', 'model'),
+                ('res_id', '=', record.id),
+                ])
+        Translation.copy([translation], default={'value': "Baz"})
+
+        with Transaction().set_context(language=self.other_language):
+            self.assertEqual(
+                Model.search([('name', '=', 'Baz')]),
+                [record])
+            self.assertEqual(
+                Model.search([('name', '=', 'Bar')]),
+                [])
+
 
 def suite():
     suite_ = unittest.TestSuite()

Reply via email to