Skip to content

Instantly share code, notes, and snippets.

@vdboor
Last active February 18, 2016 09:46
Show Gist options
  • Save vdboor/a4629d7f3d4158f3a229 to your computer and use it in GitHub Desktop.
Save vdboor/a4629d7f3d4158f3a229 to your computer and use it in GitHub Desktop.
USE https://github.com/edoburu/django-parler-rest/ NOW! THIS GIST IS DEPRECATED. It was a test to share code before making the package ----- Combining django-parler with django-rest-framework. Please tell me how that code is working for you, and if you have any improvements. The final version could be included into a 'contrib' folder of django-p…
from django.core.exceptions import ImproperlyConfigured
from rest_framework import serializers
from rest_framework.serializers import SortedDictWithMetadata
from .utils import create_translated_fields_serializer
class TranslatedFieldsField(serializers.WritableField):
"""
Exposing translated fields for a TranslatableModel in REST style.
"""
def __init__(self, *args, **kwargs):
self.serializer_class = kwargs.pop('serializer_class', None)
self.shared_model = kwargs.pop('shared_model', None)
super(TranslatedFieldsField, self).__init__(*args, **kwargs)
def initialize(self, parent, field_name):
super(TranslatedFieldsField, self).initialize(parent, field_name)
self._serializers = {}
# Expect 1-on-1 for now.
related_name = field_name
# This could all be done in __init__(), but by moving the code here,
# it's possible to auto-detect the parent model.
if self.shared_model is not None and self.serializer_class is not None:
return
# Fill in the blanks
if self.serializer_class is None:
# Auto detect parent model
if self.shared_model is None:
self.shared_model = self.parent.opts.model
# Create serializer based on shared model.
translated_model = self.shared_model._parler_meta[related_name]
self.serializer_class = create_translated_fields_serializer(self.shared_model, related_name=related_name, meta=dict(
fields = translated_model.get_translated_fields()
))
else:
self.shared_model = self.serializer_class.Meta.model
# Don't need to have a 'language_code', it will be split up already,
# so this should avoid redundant output.
if 'language_code' in self.serializer_class.base_fields:
raise ImproperlyConfigured("Serializer may not have a 'language_code' field")
def to_native(self, value):
"""
Serialize to REST format.
"""
if value is None:
return None
# Only need one serializer to create the native objects
serializer = self.serializer_class()
# Split into a dictionary per language
ret = SortedDictWithMetadata()
for translation in value.all():
ret[translation.language_code] = serializer.to_native(translation)
return ret
def from_native(self, data, files=None):
"""
Deserialize primitives -> objects.
"""
self._errors = {}
self._serializers = {}
if data is None:
return None
elif isinstance(data, dict):
# Very similar code to ModelSerializer.from_native():
translations = self.restore_fields(data, files)
if translations is not None:
translations = self.perform_validation(translations)
else:
raise serializers.ValidationError(self.error_messages['invalid'])
if not self._errors:
return translations
# No 'master' object known yet, can't store fields.
#return self.restore_object(translations)
def restore_fields(self, data, files):
translations = {}
for lang_code, model_fields in data.iteritems():
# Create a serializer per language, so errors can be stored per serializer instance.
self._serializers[lang_code] = serializer = self.serializer_class()
serializer._errors = {} # because it's .from_native() is skipped.
translations[lang_code] = serializer.restore_fields(model_fields, files)
return translations
def perform_validation(self, data):
# Runs `validate_<fieldname>()` and `validate()` methods on the serializer
for lang_code, model_fields in data.iteritems():
self._serializers[lang_code].perform_validation(model_fields)
return data
# def restore_object(self, data):
# master = self.parent.object
# for lang_code, model_fields in data.iteritems():
# translation = master._get_translated_model(lang_code, auto_create=True)
# self._serializers[lang_code].restore_object(model_fields, instance=translation)
def validate(self, data):
super(TranslatedFieldsField, self).validate(data) # checks 'required' state.
for lang_code, model_fields in data.iteritems():
self._serializers[lang_code].validate(model_fields)
from rest_framework import serializers
from .fields import TranslatedFieldsField
from myapp.modes import Region
class TranslatableModelSerializer(serializers.ModelSerializer):
"""
Serializer that makes sure that translations
from the :class:`TranslatedFieldsField` are properly saved.
"""
def save_object(self, obj, **kwargs):
"""
Extract the translations, store these into the django-parler model data.
"""
for meta in obj._parler_meta:
translations = obj._related_data.pop(meta.rel_name, {})
if translations:
for lang_code, model_fields in translations.iteritems():
translations = obj._get_translated_model(lang_code, auto_create=True, meta=meta)
for field, value in model_fields.iteritems():
setattr(translations, field, value)
return super(TranslatableModelSerializer, self).save_object(obj, **kwargs)
class RegionSerializer(TranslatableModelSerializer):
"""
A model with translated fields.
"""
translations = TranslatedFieldsField(shared_model=Region)
class Meta:
model = Region
fields = ('id', 'foo', 'bar', 'translations',)
from rest_framework import serializers
def create_translated_fields_serializer(shared_model, meta=None, related_name=None, **fields):
"""
Create a REST framework serializer class for a translated fields model.
:param shared_model: The shared model.
:type shared_model: :class:`parler.models.TranslatableModel`
"""
if not related_name:
translated_model = shared_model._parler_meta.root_model
else:
translated_model = shared_model._parler_meta[related_name].model
# Define inner Meta class
if not meta:
meta = {}
meta['model'] = translated_model
meta.setdefault('fields', ['language_code'] + translated_model.get_translated_fields())
# Define serialize class attributes
attrs = {}
attrs.update(fields)
attrs['Meta'] = type('Meta', (), meta)
# Dynamically create the serializer class
return type('{0}Serializer'.format(translated_model.__name__), (serializers.ModelSerializer,), attrs)
@igolkotek
Copy link

Hi,
Unfortunately, when I take simple
class SubjectSerializer(TranslatableModelSerializer):
translations = TranslatedFieldsField(shared_model=Subject)

class Meta:
    model = Subject
    fields = ('subject_id', 'translations',)

on serializer.data I get error:

/usr/local/lib/python2.7/dist-packages/djangorestframework-2.4.2-py2.7.egg/rest_framework/serializers.pyc in data(self)
570 self._data = [self.to_native(item) for item in obj]
571 else:
--> 572 self._data = self.to_native(obj)
573
574 return self._data

/usr/local/lib/python2.7/dist-packages/djangorestframework-2.4.2-py2.7.egg/rest_framework/serializers.pyc in to_native(self, obj)
349 field.initialize(parent=self, field_name=field_name)
350 key = self.get_field_key(field_name)
--> 351 value = field.field_to_native(obj, field_name)
352 method = getattr(self, 'transform_%s' % field_name, None)
353 if callable(method):

/usr/local/lib/python2.7/dist-packages/djangorestframework-2.4.2-py2.7.egg/rest_framework/fields.pyc in field_to_native(self, obj, field_name)
334 if self.write_only:
335 return None
--> 336 return super(WritableField, self).field_to_native(obj, field_name)
337
338 def field_from_native(self, data, files, field_name, into):

/usr/local/lib/python2.7/dist-packages/djangorestframework-2.4.2-py2.7.egg/rest_framework/fields.pyc in field_to_native(self, obj, field_name)
205
206 for component in source.split('.'):
--> 207 value = get_component(value, component)
208 if value is None:
209 break

/usr/local/lib/python2.7/dist-packages/djangorestframework-2.4.2-py2.7.egg/rest_framework/fields.pyc in get_component(obj, attr_name)
56 val = obj.get(attr_name)
57 else:
---> 58 val = getattr(obj, attr_name)
59
60 if is_simple_callable(val):

AttributeError: 'TranslatableQuerySet' object has no attribute 'subject_id'

Regards
Iho

@patrick91
Copy link

Works for me :)

Thanks!

Copy link

ghost commented Oct 22, 2014

Too works for me. Thanks.

@yellowcap
Copy link

Thanks a lot for posting this! Your code worked fine for me, but for my application I wanted a simpler version, where only one language is returned and the serialization of the translated fields looks as if they were normal fields.

Inspired by your code, I was able to create a simle read-only version that allows returning the data in a more familiar json format. Here is a gist with the described approach

https://gist.github.com/yellowcap/81c6d5f3ea1426689c80

@vdboor
Copy link
Author

vdboor commented Nov 3, 2014

Thanks for the feedback! :)

I've just updated the Gist for django-parler 1.2, which replaced the private _translations_model and _translations_field with a new _parler_meta object that supports adding translated fields on multiple object levels.

@grudelsud
Copy link

hey, nice piece of code thank you. unfortunately it doesn't work with rest framework 3.0 as they got rid of to_ and from_ native.

(btw, 👋 @patrick91 😄 )

@vdboor
Copy link
Author

vdboor commented Feb 18, 2016

@grudelsud: you can use https://github.com/edoburu/django-parler-rest/ now, this code was used as basis to create the package.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment