diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index bccfb33..8244dc1 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -11,18 +11,22 @@ from django import VERSION from django.contrib.admin.utils import get_model_from_relation from django.core.exceptions import FieldDoesNotExist from django.db import models -from django.db.models.query import ValuesIterable -from django.utils.tree import Node -from django.db.models.lookups import Lookup from django.db.models.expressions import Col +from django.db.models.lookups import Lookup +from django.db.models.query import ( + QuerySet, + ValuesIterable, + create_namedtuple_class, +) +from django.utils.tree import Node from six import moves from modeltranslation import settings from modeltranslation.fields import TranslationField from modeltranslation.utils import ( + auto_populate, build_localized_fieldname, get_language, - auto_populate, resolution_order, ) @@ -172,7 +176,7 @@ def get_field_by_colum_name(model, col): assert False, "No field found for column %s" % col -class MultilingualQuerySet(models.query.QuerySet): +class MultilingualQuerySet(QuerySet): def __init__(self, *args, **kwargs): super(MultilingualQuerySet, self).__init__(*args, **kwargs) self._post_init() @@ -203,8 +207,6 @@ class MultilingualQuerySet(models.query.QuerySet): kwargs.setdefault('_populate', self._populate) if hasattr(self, 'translation_fields'): kwargs.setdefault('translation_fields', self.translation_fields) - if hasattr(self, 'fields_to_del'): - kwargs.setdefault('fields_to_del', self.fields_to_del) if hasattr(self, 'original_fields'): kwargs.setdefault('original_fields', self.original_fields) cloned = super(MultilingualQuerySet, self)._clone() @@ -421,7 +423,6 @@ class MultilingualQuerySet(models.query.QuerySet): clone = super(MultilingualQuerySet, self)._values(*list(new_fields), **kwargs) clone.original_fields = tuple(original) clone.translation_fields = translation_fields - clone.fields_to_del = new_fields - annotation_keys - set(original) return clone # This method was not present in django-linguo @@ -437,15 +438,14 @@ class MultilingualQuerySet(models.query.QuerySet): return clone # This method was not present in django-linguo - def values_list(self, *fields, **kwargs): + def values_list(self, *fields, flat=False, named=False): if not self._rewrite: - return super(MultilingualQuerySet, self).values_list(*fields, **kwargs) - flat = kwargs.pop('flat', False) - if kwargs: - raise TypeError('Unexpected keyword arguments to values_list: %s' % (list(kwargs),)) + return super(MultilingualQuerySet, self).values_list(*fields, flat=flat, named=named) + if flat and named: + raise TypeError("'flat' and 'named' can't be used together.") if flat and len(fields) > 1: raise TypeError( - "'flat' is not valid when values_list is " "called with more than one field." + "'flat' is not valid when values_list is called with more than one field." ) selects_all = not fields if not fields: @@ -453,7 +453,11 @@ class MultilingualQuerySet(models.query.QuerySet): fields = self._get_original_fields() clone = self._values(*fields, prepare=True, selects_all=selects_all) clone._iterable_class = ( - FallbackFlatValuesListIterable if flat else FallbackValuesListIterable + FallbackNamedValuesListIterable + if named + else FallbackFlatValuesListIterable + if flat + else FallbackValuesListIterable ) return clone @@ -472,21 +476,31 @@ class FallbackValuesIterable(ValuesIterable): def __iter__(self): instance = self.X() - for row in super(FallbackValuesIterable, self).__iter__(): + + fields = self.queryset.original_fields + fields += tuple(f for f in self.queryset.query.annotation_select if f not in fields) + + for row in super().__iter__(): instance.__dict__.update(row) for key in self.queryset.translation_fields: row[key] = getattr(self.queryset.model, key).__get__(instance, None) - for key in self.queryset.fields_to_del: - del row[key] - yield row + # Restore original ordering. + yield {k: row[k] for k in fields} class FallbackValuesListIterable(FallbackValuesIterable): def __iter__(self): - fields = self.queryset.original_fields - fields += tuple(f for f in self.queryset.query.annotation_select if f not in fields) - for row in super(FallbackValuesListIterable, self).__iter__(): - yield tuple(row[f] for f in fields) + for row in super().__iter__(): + yield tuple(row.values()) + + +class FallbackNamedValuesListIterable(FallbackValuesIterable): + def __iter__(self): + for row in super().__iter__(): + names, values = row.keys(), row.values() + tuple_class = create_namedtuple_class(*names) + new = tuple.__new__ + yield new(tuple_class, values) class FallbackFlatValuesListIterable(FallbackValuesListIterable): diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 7ccf321..a8abbe4 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -19,7 +19,7 @@ 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 Q, F, Count, TextField +from django.db.models import Q, F, Value, Count, TextField from django.test import TestCase, TransactionTestCase from django.test.utils import override_settings from django.utils.translation import get_language, override, trans_real @@ -2853,6 +2853,17 @@ class TestManager(ModeltranslationTestBase): with override('de'): self.assertEqual(list(manager.values_list('title', flat=True)), ['de']) + # Values_list with named fields behave similarly. + # Also, it should preserve requested ordering. + (actual,) = manager.annotate(annotated=Value(True)).values_list( + 'title', 'annotated', 'visits', named=True + ) + expected = ('en', True, 0) + self.assertEqual(actual, expected) + self.assertEqual((actual.title, actual.annotated, actual.visits), expected) + with override('de'): + self.assertEqual(list(manager.values_list('title', 'visits', named=True)), [('de', 0)]) + # One can always turn rewrite off a = list(manager.rewrite(False).values_list('title', flat=True)) with override('de'): @@ -2869,11 +2880,12 @@ class TestManager(ModeltranslationTestBase): self.assertEqual(list(manager.values('title')), [{'title': 'de'}, {'title': 'de2'}]) # When no fields are passed, list all fields in current language. + actual = list(manager.annotate(annotated=Value(True)).values()) self.assertEqual( - list(manager.values()), + actual, [ - {'id': id1, 'title': 'en', 'visits': 0, 'description': None}, - {'id': id2, 'title': 'en2', 'visits': 0, 'description': None}, + {'id': id1, 'title': 'en', 'visits': 0, 'description': None, 'annotated': True}, + {'id': id2, 'title': 'en2', 'visits': 0, 'description': None, 'annotated': True}, ], ) # Similar for values_list