Skip to content

Commit dbc474a

Browse files
committed
Add flat translatable model serializer
1 parent 5c0a3e5 commit dbc474a

File tree

3 files changed

+258
-2
lines changed

3 files changed

+258
-2
lines changed

parler_rest/serializers.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""
22
Custom serializers suitable to translated models.
33
"""
4+
from django.core.exceptions import FieldDoesNotExist
45
from rest_framework import serializers
6+
from parler.utils.i18n import get_language
57

68
# Similar to DRF itself, expose all fields in the same manner.
79
from parler_rest.fields import TranslatedFieldsField, TranslatedField, TranslatedAbsoluteUrlField # noqa
@@ -62,3 +64,46 @@ class TranslatableModelSerializer(TranslatableModelSerializerMixin, serializers.
6264
Serializer that saves :class:`TranslatedFieldsField` automatically.
6365
"""
6466
pass
67+
68+
69+
class TranslatableFlatModelSerializer(TranslatableModelSerializerMixin, serializers.ModelSerializer):
70+
"""
71+
Serializer that returns a flat model and saves the translations to the activated language.
72+
"""
73+
74+
def _pop_translated_data(self):
75+
translated_data = {}
76+
language_code = self.validated_data.pop('language_code', None) or get_language()
77+
translated_fields = self._pop_translatable_fields()
78+
for meta in self.Meta.model._parler_meta:
79+
translations = {}
80+
if translated_fields:
81+
translations[language_code] = translated_fields
82+
translated_data[meta.rel_name] = translations
83+
return translated_data
84+
85+
def _pop_translatable_fields(self):
86+
"""
87+
Separate translated fields and value from the shared object data.
88+
"""
89+
translated_fields = {}
90+
fields = (field for field in self.Meta.model._parler_meta.get_all_fields()
91+
if field in self.validated_data)
92+
for field in fields:
93+
translated_fields[field] = self.validated_data.pop(field)
94+
return translated_fields
95+
96+
def build_field(self, field_name, info, model_class, nested_depth):
97+
"""
98+
Build fields for translatable fields when not explicitly defined
99+
"""
100+
field = None
101+
if field_name not in ('id', 'master'):
102+
try:
103+
field = model_class._parler_meta.root_model._meta.get_field(field_name)
104+
except FieldDoesNotExist:
105+
pass
106+
if field is not None:
107+
return self.build_standard_field(field_name, field)
108+
return super(TranslatableFlatModelSerializer, self).build_field(
109+
field_name, info, model_class, nested_depth)

testproj/serializers.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from rest_framework import serializers
22

3-
from parler_rest.serializers import TranslatableModelSerializer, TranslatedFieldsField, TranslatedField
3+
from parler_rest.serializers import TranslatableModelSerializer, TranslatedFieldsField, TranslatedField, \
4+
TranslatableFlatModelSerializer
45

56
from .models import Country, Picture
67

@@ -65,3 +66,47 @@ class PictureCaptionSerializer(TranslatableModelSerializer):
6566
class Meta:
6667
model = Picture
6768
fields = ('image_nr', 'caption')
69+
70+
71+
class FlatCountryTranslatedSerializer(TranslatableFlatModelSerializer):
72+
"""
73+
A serializer with a flat structure returning a single language for the translations
74+
"""
75+
76+
class Meta:
77+
model = Country
78+
fields = ('pk', 'country_code', 'language_code', 'name', 'url')
79+
80+
81+
class FlatCountryExplicitLangTranslatedSerializer(TranslatableFlatModelSerializer):
82+
"""
83+
A serializer where the possible language choice for the language_code field is explicit assigned
84+
"""
85+
LANGUAGE_CHOICES = (
86+
('en', 'english'),
87+
('es', 'spanish'),
88+
('fr', 'french'),
89+
)
90+
language_code = serializers.ChoiceField(choices=LANGUAGE_CHOICES)
91+
92+
class Meta:
93+
model = Country
94+
fields = ('pk', 'country_code', 'language_code', 'name', 'url')
95+
96+
97+
class FlatCountryNoLanguageCodeTranslatedSerializer(TranslatableFlatModelSerializer):
98+
"""
99+
A serializer without a language_code field declared
100+
"""
101+
102+
class Meta:
103+
model = Country
104+
fields = ('pk', 'country_code', 'name', 'url')
105+
106+
107+
class FlatContinentCountriesTranslatedSerializer(serializers.Serializer):
108+
"""
109+
A flat serializer with a nested translation serializer
110+
"""
111+
continent = serializers.CharField()
112+
countries = FlatCountryTranslatedSerializer(many=True)

testproj/tests.py

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
CountryAutoSharedModelTranslatedSerializer,
1515
CountryExplicitTranslatedSerializer,
1616
ContinentCountriesTranslatedSerializer,
17-
PictureCaptionSerializer,
17+
PictureCaptionSerializer, FlatContinentCountriesTranslatedSerializer, FlatCountryTranslatedSerializer,
18+
FlatCountryNoLanguageCodeTranslatedSerializer, FlatCountryExplicitLangTranslatedSerializer,
1819
)
1920

2021

@@ -292,3 +293,168 @@ def test_translation_deserialization(self):
292293
self.assertEqual(instance.caption, "Spanien")
293294
instance.set_current_language('es')
294295
self.assertEqual(instance.caption, "Spain") # fallback on default
296+
297+
298+
class FlatCountryTranslatedSerializerTestCase(TestCase):
299+
# Disable cache as due to automatic db rollback the instance pk
300+
# is the same for all tests and with the cache we'd mistakenly
301+
# skips saves after the first test.
302+
@override_parler_settings(PARLER_ENABLE_CACHING=False)
303+
def setUp(self):
304+
self.instance = Country.objects.create(
305+
country_code='ES', name="Spain",
306+
url="http://en.wikipedia.org/wiki/Spain"
307+
)
308+
self.instance.set_current_language('es')
309+
self.instance.name = "España"
310+
self.instance.url = "http://es.wikipedia.org/wiki/España"
311+
self.instance.save()
312+
313+
def test_en_translations_serialization(self):
314+
self.instance.set_current_language('en')
315+
expected_en = {
316+
'pk': self.instance.pk,
317+
'country_code': 'ES',
318+
'language_code': 'en',
319+
'name': "Spain",
320+
'url': "http://en.wikipedia.org/wiki/Spain"
321+
}
322+
serializer = FlatCountryTranslatedSerializer(self.instance)
323+
self.assertDictEqual(serializer.data, expected_en)
324+
325+
def test_es_translations_serialization(self):
326+
self.instance.set_current_language('es')
327+
expected_es = {
328+
'pk': self.instance.pk,
329+
'country_code': 'ES',
330+
'language_code': 'es',
331+
'name': "España",
332+
'url': "http://es.wikipedia.org/wiki/España"
333+
}
334+
serializer = FlatCountryTranslatedSerializer(self.instance)
335+
self.assertDictEqual(serializer.data, expected_es)
336+
337+
def test_translations_serialization_no_language_code(self):
338+
self.instance.set_current_language('es')
339+
serializer = FlatCountryNoLanguageCodeTranslatedSerializer(self.instance)
340+
expected = {
341+
'pk': self.instance.pk,
342+
'country_code': 'ES',
343+
'name': "España",
344+
'url': "http://es.wikipedia.org/wiki/España"
345+
}
346+
self.assertDictEqual(serializer.data, expected)
347+
348+
def test_language_code_validation(self):
349+
data = {
350+
'country_code': 'es',
351+
'language_code': 'es',
352+
'name': 'España',
353+
'url': "http://es.wikipedia.org/wiki/España"
354+
}
355+
serializer = FlatCountryTranslatedSerializer(data=data)
356+
self.assertTrue(serializer.is_valid(), serializer.errors)
357+
358+
def test_language_code_invalid(self):
359+
data = {
360+
'country_code': 'es',
361+
'language_code': 'fr',
362+
'name': 'Espagne',
363+
'url': "http://fr.wikipedia.org/wiki/Espagne"
364+
}
365+
serializer = FlatCountryTranslatedSerializer(data=data)
366+
self.assertFalse(serializer.is_valid())
367+
self.assertIn('language_code', serializer.errors)
368+
self.assertEqual(serializer.errors['language_code'][0], '"fr" is not a valid choice.')
369+
370+
def test_translated_fields_validation(self):
371+
data = {
372+
'country_code': 'FR',
373+
'language_code': 'en',
374+
'url': "es.wikipedia.org/wiki/Francia"
375+
}
376+
serializer = FlatCountryTranslatedSerializer(data=data)
377+
self.assertFalse(serializer.is_valid())
378+
self.assertIn('name', serializer.errors)
379+
self.assertEqual(serializer.errors['name'][0], 'This field is required.')
380+
self.assertIn('url', serializer.errors)
381+
self.assertEqual(serializer.errors['url'][0], 'Enter a valid URL.')
382+
383+
def test_translation_saving_on_create(self):
384+
data = {
385+
'country_code': 'FR',
386+
'language_code': 'es',
387+
'name': "Francia",
388+
'url': "http://es.wikipedia.org/wiki/Francia"
389+
}
390+
serializer = FlatCountryTranslatedSerializer(data=data)
391+
self.assertTrue(serializer.is_valid(), serializer.errors)
392+
instance = serializer.save()
393+
instance = Country.objects.get(pk=instance.pk)
394+
instance.set_current_language('es')
395+
self.assertEqual(instance.name, "Francia")
396+
self.assertEqual(instance.url, "http://es.wikipedia.org/wiki/Francia")
397+
398+
def test_translation_saving_on_update(self):
399+
data = {
400+
'country_code': 'E',
401+
'language_code': 'es',
402+
'name': "Hispania",
403+
'url': "http://es.wikipedia.org/wiki/Hispania"
404+
}
405+
serializer = FlatCountryTranslatedSerializer(self.instance, data=data)
406+
self.assertTrue(serializer.is_valid(), serializer.errors)
407+
instance = serializer.save()
408+
instance = Country.objects.get(pk=instance.pk)
409+
self.assertEqual(instance.country_code, 'E') # also check if shared model is updated
410+
411+
instance.set_current_language('es')
412+
self.assertEqual(instance.name, "Hispania")
413+
self.assertEqual(instance.url, "http://es.wikipedia.org/wiki/Hispania")
414+
415+
def test_translations_saving_on_update_with_new_translation(self):
416+
data = {
417+
'country_code': 'ES',
418+
'language_code': 'fr',
419+
'name': "Espagne",
420+
'url': "http://fr.wikipedia.org/wiki/Espagne"
421+
}
422+
# Language choices are automatically verified against settings,
423+
# thus using a serializer with explicitly set language choices
424+
serializer = FlatCountryExplicitLangTranslatedSerializer(self.instance, data=data)
425+
self.assertTrue(serializer.is_valid(), serializer.errors)
426+
instance = serializer.save()
427+
instance = Country.objects.get(pk=instance.pk)
428+
instance.set_current_language('fr')
429+
self.assertEqual(instance.name, "Espagne")
430+
self.assertEqual(instance.url, "http://fr.wikipedia.org/wiki/Espagne")
431+
432+
def test_translation_saving_on_create_no_language_code(self):
433+
data = {
434+
'country_code': 'FR',
435+
'name': "French",
436+
'url': "http://en.wikipedia.org/wiki/French"
437+
}
438+
serializer = FlatCountryNoLanguageCodeTranslatedSerializer(data=data)
439+
self.assertTrue(serializer.is_valid(), serializer.errors)
440+
instance = serializer.save()
441+
instance = Country.objects.get(pk=instance.pk)
442+
instance.set_current_language('en') # The fallback language, set via get_language
443+
self.assertEqual(instance.name, "French")
444+
self.assertEqual(instance.url, "http://en.wikipedia.org/wiki/French")
445+
446+
def test_nested__translatedserializer(self):
447+
data = {
448+
"continent": "Europe",
449+
"countries": [{
450+
'country_code': 'FR',
451+
'language_code': 'en',
452+
'name': "France",
453+
'url': "http://en.wikipedia.org/wiki/France"
454+
}]
455+
}
456+
serializer = FlatContinentCountriesTranslatedSerializer(data=data)
457+
self.assertTrue(serializer.is_valid(), serializer.errors)
458+
nested_data = serializer.validated_data['countries'][0]
459+
expected = data['countries'][0]
460+
self.assertDictEqual(nested_data, expected)

0 commit comments

Comments
 (0)