-
-
Save vdboor/a4629d7f3d4158f3a229 to your computer and use it in GitHub Desktop.
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) |
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
Works for me :)
Thanks!
Too works for me. Thanks.
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
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.
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 ๐ )
@grudelsud: you can use https://github.com/edoburu/django-parler-rest/ now, this code was used as basis to create the package.
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-parler package or be released as separate package.