I have a few models that are translatable.
The way we implemented it was: for each translatable model, there is a ModelTranslation model.
E.g. for the Car
model exists a CarTranslation
model with FK to Car
.
The translation models also have as fields: the language, and any text/char fields of the original model.
E.g. if the Car
model looks like this
class Car(models.Model):
name = models.CharField()
description = models.TextField()
the CarTranslation
then looks like this:
class CarTranslation(models.Model):
car = models.ForeignKey(to=Car)
language = models.CharField(choices=LANGUAGE_CHOICES)
name = models.CharField()
description = models.TextField()
In views that return translatable data, we check what language the user has defined: if it's english (the default) we return Car
as it is; if it's not english, we return the translated data in CarTranslation
for the user language (if it exists, otherwise default to english).
We have an old implementation of this that I want to improve: right now we add the translations in the serializer context and then, in the serializer, we check if there are translations, otherwise return the default.
But I don't like this.
I think a much better solution would be using custom model managers/querysets.
I'm trying this:
from django.db.models.functions import Coalesce
from django.db.models import F, Subquery
class CarQuerySet(models.QuerySet):
def with_translation(self, language):
from cars.models import CarTranslation
if language == "en":
return self
translation_query = CarTranslation.objects.filter(language=language, car=OuterRef("pk"))
return self.annotate(
name=Coalesce(Subquery(translation_query.values("name")[:1]), F("name")),
description=Coalesce(Subquery(translation_query.values("description")[:1]), F("description")),
)
And then use it in the views like this:
class CarView(ListAPIView):
def get_queryset(self):
user = self.request.user
language = user.language
return Car.objects.with_translation(language)
But it produces this error: ValueError: The annotation 'name' conflicts with a field on the model.
I've also tried annotating with a different name, then not selecting the conflicting columns and returning the annotated values instead:
from django.db.models.functions import Coalesce
from django.db.models import F, Subquery
class CarQuerySet(models.QuerySet):
def with_translation(self, language):
from cars.models import CarTranslation
if language == "en":
return self
translation_query = CarTranslation.objects.filter(language=language, car==OuterRef("pk"))
queryset = self.annotate(
translated_name=Coalesce(Subquery(translation_query.values("name")[:1]), F("name")),
translated_description=Coalesce(Subquery(translation_query.values("description")[:1]), F("description")),
)
return queryset.values(
*[field.name for field in self.model._meta.fields if field.name not in ["name", "description"]]
name=F("translated_name"),
description=F("translated_description"),
)
but it produces the same exact error.
Any idea how I can return either the default or the translated value from the custom queryset?
You can not work with the name of a field that already exists, this could also result in a lot of name clashes, and behavior that is not ambiguous for the runtime environment, but makes it very complicated for the programmer.
from django.db.models import F, Subquery
from django.db.models.functions import Coalesce
class CarQuerySet(models.QuerySet):
def with_translation(self, language):
from cars.models import CarTranslation
if language == 'en':
return self.annotate(custom_name=F('name'), custom_description=F('description'))
translation_query = CarTranslation.objects.filter(
language=language, car_id=OuterRef('pk')
)
return self.annotate(
custom_name=Coalesce(
Subquery(translation_query.values('name')[:1]), F('name')
),
custom_description=Coalesce(
Subquery(translation_query.values('description')[:1]),
F('description'),
),
)
Then in the serializer, we can use the custom_name
and custom_description
:
class CarSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='custom_name')
description = serializers.CharField(source='custom_description')
class Meta:
model = Car
fields = ['name', 'description']