Skip to content

Allow for defining translatable fields loosly on the model #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions parler_rest/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""
Custom serializers suitable to translated models.
"""
from django.core.exceptions import FieldDoesNotExist
from rest_framework import serializers
from parler.utils.i18n import get_language

# Similar to DRF itself, expose all fields in the same manner.
from parler_rest.fields import TranslatedFieldsField, TranslatedField, TranslatedAbsoluteUrlField # noqa
Expand Down Expand Up @@ -62,3 +64,46 @@ class TranslatableModelSerializer(TranslatableModelSerializerMixin, serializers.
Serializer that saves :class:`TranslatedFieldsField` automatically.
"""
pass


class TranslatableFlatModelSerializer(TranslatableModelSerializerMixin, serializers.ModelSerializer):
"""
Serializer that returns a flat model and saves the translations to the activated language.
"""

def _pop_translated_data(self):
translated_data = {}
language_code = self.validated_data.pop('language_code', None) or get_language()
translated_fields = self._pop_translatable_fields()
for meta in self.Meta.model._parler_meta:
translations = {}
if translated_fields:
translations[language_code] = translated_fields
translated_data[meta.rel_name] = translations
return translated_data

def _pop_translatable_fields(self):
"""
Separate translated fields and value from the shared object data.
"""
translated_fields = {}
fields = (field for field in self.Meta.model._parler_meta.get_all_fields()
if field in self.validated_data)
for field in fields:
translated_fields[field] = self.validated_data.pop(field)
return translated_fields

def build_field(self, field_name, info, model_class, nested_depth):
"""
Build fields for translatable fields when not explicitly defined
"""
field = None
if field_name not in ('id', 'master'):
try:
field = model_class._parler_meta.root_model._meta.get_field(field_name)
except FieldDoesNotExist:
pass
if field is not None:
return self.build_standard_field(field_name, field)
return super(TranslatableFlatModelSerializer, self).build_field(
field_name, info, model_class, nested_depth)
47 changes: 46 additions & 1 deletion testproj/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import serializers

from parler_rest.serializers import TranslatableModelSerializer, TranslatedFieldsField, TranslatedField
from parler_rest.serializers import TranslatableModelSerializer, TranslatedFieldsField, TranslatedField, \
TranslatableFlatModelSerializer

from .models import Country, Picture

Expand Down Expand Up @@ -65,3 +66,47 @@ class PictureCaptionSerializer(TranslatableModelSerializer):
class Meta:
model = Picture
fields = ('image_nr', 'caption')


class FlatCountryTranslatedSerializer(TranslatableFlatModelSerializer):
"""
A serializer with a flat structure returning a single language for the translations
"""

class Meta:
model = Country
fields = ('pk', 'country_code', 'language_code', 'name', 'url')


class FlatCountryExplicitLangTranslatedSerializer(TranslatableFlatModelSerializer):
"""
A serializer where the possible language choice for the language_code field is explicit assigned
"""
LANGUAGE_CHOICES = (
('en', 'english'),
('es', 'spanish'),
('fr', 'french'),
)
language_code = serializers.ChoiceField(choices=LANGUAGE_CHOICES)

class Meta:
model = Country
fields = ('pk', 'country_code', 'language_code', 'name', 'url')


class FlatCountryNoLanguageCodeTranslatedSerializer(TranslatableFlatModelSerializer):
"""
A serializer without a language_code field declared
"""

class Meta:
model = Country
fields = ('pk', 'country_code', 'name', 'url')


class FlatContinentCountriesTranslatedSerializer(serializers.Serializer):
"""
A flat serializer with a nested translation serializer
"""
continent = serializers.CharField()
countries = FlatCountryTranslatedSerializer(many=True)
168 changes: 167 additions & 1 deletion testproj/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
CountryAutoSharedModelTranslatedSerializer,
CountryExplicitTranslatedSerializer,
ContinentCountriesTranslatedSerializer,
PictureCaptionSerializer,
PictureCaptionSerializer, FlatContinentCountriesTranslatedSerializer, FlatCountryTranslatedSerializer,
FlatCountryNoLanguageCodeTranslatedSerializer, FlatCountryExplicitLangTranslatedSerializer,
)


Expand Down Expand Up @@ -292,3 +293,168 @@ def test_translation_deserialization(self):
self.assertEqual(instance.caption, "Spanien")
instance.set_current_language('es')
self.assertEqual(instance.caption, "Spain") # fallback on default


class FlatCountryTranslatedSerializerTestCase(TestCase):
# Disable cache as due to automatic db rollback the instance pk
# is the same for all tests and with the cache we'd mistakenly
# skips saves after the first test.
@override_parler_settings(PARLER_ENABLE_CACHING=False)
def setUp(self):
self.instance = Country.objects.create(
country_code='ES', name="Spain",
url="http://en.wikipedia.org/wiki/Spain"
)
self.instance.set_current_language('es')
self.instance.name = "España"
self.instance.url = "http://es.wikipedia.org/wiki/España"
self.instance.save()

def test_en_translations_serialization(self):
self.instance.set_current_language('en')
expected_en = {
'pk': self.instance.pk,
'country_code': 'ES',
'language_code': 'en',
'name': "Spain",
'url': "http://en.wikipedia.org/wiki/Spain"
}
serializer = FlatCountryTranslatedSerializer(self.instance)
self.assertDictEqual(serializer.data, expected_en)

def test_es_translations_serialization(self):
self.instance.set_current_language('es')
expected_es = {
'pk': self.instance.pk,
'country_code': 'ES',
'language_code': 'es',
'name': "España",
'url': "http://es.wikipedia.org/wiki/España"
}
serializer = FlatCountryTranslatedSerializer(self.instance)
self.assertDictEqual(serializer.data, expected_es)

def test_translations_serialization_no_language_code(self):
self.instance.set_current_language('es')
serializer = FlatCountryNoLanguageCodeTranslatedSerializer(self.instance)
expected = {
'pk': self.instance.pk,
'country_code': 'ES',
'name': "España",
'url': "http://es.wikipedia.org/wiki/España"
}
self.assertDictEqual(serializer.data, expected)

def test_language_code_validation(self):
data = {
'country_code': 'es',
'language_code': 'es',
'name': 'España',
'url': "http://es.wikipedia.org/wiki/España"
}
serializer = FlatCountryTranslatedSerializer(data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)

def test_language_code_invalid(self):
data = {
'country_code': 'es',
'language_code': 'fr',
'name': 'Espagne',
'url': "http://fr.wikipedia.org/wiki/Espagne"
}
serializer = FlatCountryTranslatedSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn('language_code', serializer.errors)
self.assertEqual(serializer.errors['language_code'][0], '"fr" is not a valid choice.')

def test_translated_fields_validation(self):
data = {
'country_code': 'FR',
'language_code': 'en',
'url': "es.wikipedia.org/wiki/Francia"
}
serializer = FlatCountryTranslatedSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn('name', serializer.errors)
self.assertEqual(serializer.errors['name'][0], 'This field is required.')
self.assertIn('url', serializer.errors)
self.assertEqual(serializer.errors['url'][0], 'Enter a valid URL.')

def test_translation_saving_on_create(self):
data = {
'country_code': 'FR',
'language_code': 'es',
'name': "Francia",
'url': "http://es.wikipedia.org/wiki/Francia"
}
serializer = FlatCountryTranslatedSerializer(data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)
instance = serializer.save()
instance = Country.objects.get(pk=instance.pk)
instance.set_current_language('es')
self.assertEqual(instance.name, "Francia")
self.assertEqual(instance.url, "http://es.wikipedia.org/wiki/Francia")

def test_translation_saving_on_update(self):
data = {
'country_code': 'E',
'language_code': 'es',
'name': "Hispania",
'url': "http://es.wikipedia.org/wiki/Hispania"
}
serializer = FlatCountryTranslatedSerializer(self.instance, data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)
instance = serializer.save()
instance = Country.objects.get(pk=instance.pk)
self.assertEqual(instance.country_code, 'E') # also check if shared model is updated

instance.set_current_language('es')
self.assertEqual(instance.name, "Hispania")
self.assertEqual(instance.url, "http://es.wikipedia.org/wiki/Hispania")

def test_translations_saving_on_update_with_new_translation(self):
data = {
'country_code': 'ES',
'language_code': 'fr',
'name': "Espagne",
'url': "http://fr.wikipedia.org/wiki/Espagne"
}
# Language choices are automatically verified against settings,
# thus using a serializer with explicitly set language choices
serializer = FlatCountryExplicitLangTranslatedSerializer(self.instance, data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)
instance = serializer.save()
instance = Country.objects.get(pk=instance.pk)
instance.set_current_language('fr')
self.assertEqual(instance.name, "Espagne")
self.assertEqual(instance.url, "http://fr.wikipedia.org/wiki/Espagne")

def test_translation_saving_on_create_no_language_code(self):
data = {
'country_code': 'FR',
'name': "French",
'url': "http://en.wikipedia.org/wiki/French"
}
serializer = FlatCountryNoLanguageCodeTranslatedSerializer(data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)
instance = serializer.save()
instance = Country.objects.get(pk=instance.pk)
instance.set_current_language('en') # The fallback language, set via get_language
self.assertEqual(instance.name, "French")
self.assertEqual(instance.url, "http://en.wikipedia.org/wiki/French")

def test_nested__translatedserializer(self):
data = {
"continent": "Europe",
"countries": [{
'country_code': 'FR',
'language_code': 'en',
'name': "France",
'url': "http://en.wikipedia.org/wiki/France"
}]
}
serializer = FlatContinentCountriesTranslatedSerializer(data=data)
self.assertTrue(serializer.is_valid(), serializer.errors)
nested_data = serializer.validated_data['countries'][0]
expected = data['countries'][0]
self.assertDictEqual(nested_data, expected)