changeset 7ab27a034d8f in trytond:5.8
details: https://hg.tryton.org/trytond?cmd=changeset&node=7ab27a034d8f
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 71eda7e5d8dd -r 7ab27a034d8f trytond/ir/translation.py
--- a/trytond/ir/translation.py Wed Mar 03 18:09:27 2021 +0100
+++ b/trytond/ir/translation.py Sat Feb 20 00:48:50 2021 +0100
@@ -404,32 +404,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
 
@@ -437,7 +439,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),
@@ -445,9 +448,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([
@@ -456,17 +458,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()
@@ -474,17 +475,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 71eda7e5d8dd -r 7ab27a034d8f trytond/model/fields/field.py
--- a/trytond/model/fields/field.py     Wed Mar 03 18:09:27 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.cache import LRUDictTransaction
 from trytond.const import OPERATORS
 from trytond.pool import Pool
@@ -16,6 +19,8 @@
 from trytond.tools.string_ import StringPartitioned, LazyString
 from trytond.transaction import Transaction
 
+_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 71eda7e5d8dd -r 7ab27a034d8f trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.py    Wed Mar 03 18:09:27 2021 +0100
+++ b/trytond/tests/test_modelsql.py    Sat Feb 20 00:48:50 2021 +0100
@@ -769,6 +769,64 @@
         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')]),
+                [])
+
+    @with_transaction()
     def test_search_fill_transaction_cache(self):
         "Test search fill the transaction cache"
         pool = Pool()

Reply via email to