Repository: deschler/django-modeltranslation

Issue:
Error "'Cast' object has no attribute 'split'" when using Cast in values_list()
We're using wagtail with [wagtail-modeltranslation](https://github.com/infoportugal/wagtail-modeltranslation), which uses [django-modeltranslation](https://github.com/deschler/django-modeltranslation), and we've stumbled over this error:

    2023-02-08 16:41:30 ERROR django.request: Internal Server Error: /admin/ (log.py:224)
    Traceback (most recent call last):
      File "***/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
        response = get_response(request)
      File "***/lib/python3.8/site-packages/django/core/handlers/base.py", line 204, in _get_response
        response = response.render()
      File "***/lib/python3.8/site-packages/wagtail/admin/auth.py", line 197, in overridden_render
        return render()
      ...
      File "***/lib/python3.8/site-packages/django/template/library.py", line 192, in render
        output = self.func(*resolved_args, **resolved_kwargs)
      File "***/lib/python3.8/site-packages/wagtail/admin/templatetags/wagtailadmin_tags.py", line 920, in component
        return obj.render_html(context)
      File "***/lib/python3.8/site-packages/wagtail/admin/ui/components.py", line 15, in render_html
        context_data = self.get_context_data(parent_context)
      File "***/lib/python3.8/site-packages/wagtail/admin/views/home.py", line 103, in get_context_data
        user_perms.revisions_for_moderation()
      File "***/lib/python3.8/site-packages/wagtail/models/__init__.py", line 2953, in revisions_for_moderation
        object_id__in=Page.objects.filter(only_my_sections).values_list(
      File "***/lib/python3.8/site-packages/modeltranslation/manager.py", line 450, in values_list
        clone = self._values(*fields, prepare=True, selects_all=selects_all)
      File "***/lib/python3.8/site-packages/modeltranslation/manager.py", line 419, in _values
        clone = super(MultilingualQuerySet, self)._values(*list(new_fields), **kwargs)
      File "***/lib/python3.8/site-packages/django/db/models/query.py", line 835, in _values
        clone.query.set_values(fields)
      File "***/lib/python3.8/site-packages/django/db/models/sql/query.py", line 2272, in set_values
        self.add_fields(field_names, True)
      File "***/lib/python3.8/site-packages/django/db/models/sql/query.py", line 1955, in add_fields
        join_info = self.setup_joins(name.split(LOOKUP_SEP), opts, alias, allow_many=allow_m2m)
    AttributeError: 'Cast' object has no attribute 'split'

django-modeltranslation does not yet support `values()` or `values_list()` calls with expressions, like django's `Cast(...)`-operator ([docs](https://docs.djangoproject.com/en/3.2/ref/models/database-functions/#cast)) which is used by wagtail above.

I've already fixed this locally and I will make a PR shortly.

Discussion:

Patch:
diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py
index a17a815..064a66b 100644
--- a/modeltranslation/manager.py
+++ b/modeltranslation/manager.py
@@ -406,8 +406,8 @@ class MultilingualQuerySet(QuerySet):
         return super(MultilingualQuerySet, self).only(*fields)
 
     # This method was not present in django-linguo
-    def raw_values(self, *fields):
-        return super(MultilingualQuerySet, self).values(*fields)
+    def raw_values(self, *fields, **expressions):
+        return super(MultilingualQuerySet, self).values(*fields, **expressions)
 
     def _values(self, *original, **kwargs):
         selects_all = kwargs.pop('selects_all', False)
@@ -429,6 +429,7 @@ class MultilingualQuerySet(QuerySet):
         if not fields:
             # Emulate original queryset behaviour: get all fields that are not translation fields
             fields = self._get_original_fields()
+        fields += tuple(expressions)
         clone = self._values(*fields, prepare=True, selects_all=selects_all, **expressions)
         clone._iterable_class = FallbackValuesIterable
         return clone
@@ -447,7 +448,25 @@ class MultilingualQuerySet(QuerySet):
         if not fields:
             # Emulate original queryset behaviour: get all fields that are not translation fields
             fields = self._get_original_fields()
-        clone = self._values(*fields, prepare=True, selects_all=selects_all)
+
+        field_names = {f for f in fields if not hasattr(f, 'resolve_expression')}
+        _fields = []
+        expressions = {}
+        counter = 1
+        for field in fields:
+            if hasattr(field, 'resolve_expression'):
+                field_id_prefix = getattr(field, 'default_alias', field.__class__.__name__.lower())
+                while True:
+                    field_id = field_id_prefix + str(counter)
+                    counter += 1
+                    if field_id not in field_names:
+                        break
+                expressions[field_id] = field
+                _fields.append(field_id)
+            else:
+                _fields.append(field)
+
+        clone = self._values(*_fields, prepare=True, selects_all=selects_all, **expressions)
         clone._iterable_class = (
             FallbackNamedValuesListIterable
             if named


Test patch:
diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py
index 9bc06d7..0632804 100644
--- a/modeltranslation/tests/tests.py
+++ b/modeltranslation/tests/tests.py
@@ -17,7 +17,8 @@ from django.core.files.storage import default_storage
 from django.core.management import call_command
 from django.core.management.base import CommandError
 from django.db import IntegrityError
-from django.db.models import Count, F, Q, TextField, Value
+from django.db.models import CharField, Count, F, Q, TextField, Value
+from django.db.models.functions import Cast
 from django.test import TestCase, TransactionTestCase
 from django.test.utils import override_settings
 from django.utils.translation import get_language, override, trans_real
@@ -2993,6 +2994,29 @@ class TestManager(ModeltranslationTestBase):
             ('foo', 2)
         ]
 
+    def test_values_with_expressions(self):
+        manager = models.ManagerTestModel.objects
+        id1 = manager.create(title_en='en', title_de='de').pk
+
+        raw_obj = manager.raw_values('title', str_pk=Cast("pk", output_field=CharField()))[0]
+        obj = manager.values('title', str_pk=Cast("pk", output_field=CharField()))[0]
+        with override('de'):
+            raw_obj2 = manager.raw_values('title', str_pk=Cast("pk", output_field=CharField()))[0]
+            obj2 = manager.values('title', str_pk=Cast("pk", output_field=CharField()))[0]
+
+        # Raw_values returns real database values regardless of current language
+        assert raw_obj['title'] == raw_obj2['title']
+        assert raw_obj['str_pk'] == raw_obj2['str_pk']
+        # Values present language-aware data, from the moment of retrieval
+        assert obj['title'] == 'en'
+        assert obj['str_pk'] == str(id1)
+        assert obj2['title'] == 'de'
+
+        # Values_list behave similarly
+        assert list(manager.values_list('title', Cast("pk", output_field=CharField()))) == [('en', str(id1))]
+        with override('de'):
+            assert list(manager.values_list('title', Cast("pk", output_field=CharField()))) == [('de', str(id1))]
+
     def test_custom_manager(self):
         """Test if user-defined manager is still working"""
         n = models.CustomManagerTestModel(title='')