pythondjangodjango-rest-frameworkdjango-modeltranslationdjango-hvad

Django Rest Framework and Django-Hvad


so i needed to had some model-translation support for my DRF API and i started using django-hvad.

It seems to work well with my django application but i am getting some issues with the DRF APi.

I am trying to create a simple POST request and i am getting a error:

Accessing a translated field requires that the instance has a translation loaded, or a valid translation in current language (en) loadable from the database

Here are my models, serializers and viewsets:

Model:

class Mission(TranslatableModel):
    translations = TranslatedFields(
        mission=models.CharField(max_length=255, help_text="Mission name"),
    )

    def __unicode__(self):
        return self.lazy_translation_getter('mission', str(self.pk))

Serializer:

class MissionSerializer(serializers.ModelSerializer):
    mission = serializers.CharField(source='mission')

    class Meta:
        model = Mission

Viewset:

class MissionViewSet(viewsets.ModelViewSet):
    queryset = Mission.objects.language().all()
    serializer_class = MissionSerializer
    authentication_classes = (NoAuthentication,)
    permission_classes = (AllowAny,)

    def get_queryset(self):
        # Set Language For Translations
        user_language = self.request.GET.get('language')
        if user_language:
            translation.activate(user_language)
        return Mission.objects.language().all()

Does anyone know how i can get around this?? I am also opened to other suggested apps known to work but i would really like to have this one working


Solution

  • I got this to work thanks to the Spectras here https://github.com/KristianOellegaard/django-hvad/issues/211

    The issue, I guess is DRF tries to do some introspection on the model. I do use DRF in a project of mine, on a TranslatableModel. It needs some glue to work properly. I once suggested adding that to hvad, but we concluded that that would be overextending the feature set. Maybe another module some day, but I don't have enough time to maintain both hvad and that.

    It's been some time since I implemented it, so here it is as is:

    # hvad compatibility for rest_framework - JHA
    
    class TranslatableModelSerializerOptions(serializers.ModelSerializerOptions):
        def __init__(self, meta):
            super(TranslatableModelSerializerOptions, self).__init__(meta)
            # We need this ugly hack as ModelSerializer hardcodes a read_only_fields check
            self.translated_read_only_fields = getattr(meta, 'translated_read_only_fields', ())
            self.translated_write_only_fields = getattr(meta, 'translated_write_only_fields', ())
    
    class HyperlinkedTranslatableModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
        def __init__(self, meta):
            super(HyperlinkedTranslatableModelSerializerOptions, self).__init__(meta)
            # We need this ugly hack as ModelSerializer hardcodes a read_only_fields check
            self.translated_read_only_fields = getattr(meta, 'translated_read_only_fields', ())
            self.translated_write_only_fields = getattr(meta, 'translated_write_only_fields', ())
    
    class TranslatableModelMixin(object):
        def get_default_fields(self):
            fields = super(TranslatableModelMixin, self).get_default_fields()
            fields.update(self._get_translated_fields())
            return fields
    
        def _get_translated_fields(self):
            ret = OrderedDict()
            trans_model = self.opts.model._meta.translations_model
            opts = trans_model._meta
    
            forward_rels = [field for field in opts.fields
                            if field.serialize and not field.name in ('id', 'master')]
    
            for trans_field in forward_rels:
                if trans_field.rel:
                    raise RuntimeError()
                field = self.get_field(trans_field)
                if field:
                    ret[trans_field.name] = field
    
            for field_name in self.opts.translated_read_only_fields:
                assert field_name in ret
                ret[field_name].read_only = True
    
            for field_name in self.opts.translated_write_only_fields:
                assert field_name in ret
                ret[field_name].write_only = True
    
            return ret
    
        def restore_object(self, attrs, instance=None):
            new_attrs = attrs.copy()
            lang = attrs['language_code']
            del new_attrs['language_code']
    
            if instance is None:
                # create an empty instance, pre-translated
                instance = self.opts.model()
                instance.translate(lang)
            else:
                # check we are updating the correct translation
                tcache = self.opts.model._meta.translations_cache
                translation = getattr(instance, tcache, None)
                if not translation or translation.language_code != lang:
                    # nope, get the translation we are updating, or create it if needed
                    try:
                        translation = instance.translations.get_language(lang)
                    except instance.translations.model.DoesNotExist:
                        instance.translate(lang)
                    else:
                        setattr(instance, tcache, translation)
    
            return super(TranslatableModelMixin, self).restore_object(new_attrs, instance)
    
    class TranslatableModelSerializer(TranslatableModelMixin, serializers.ModelSerializer):
        _options_class = TranslatableModelSerializerOptions
    
    class HyperlinkedTranslatableModelSerializer(TranslatableModelMixin,
                                                 serializers.HyperlinkedModelSerializer):
        _options_class = HyperlinkedTranslatableModelSerializerOptions
    

    From there, you just inherit your serializers from TranslatableModelSerializer or HyperlinkedTranslatableModelSerializer. When POSTing, you should simple add language_code as a normal field as part of your JSON / XML / whatever.

    The main trick is in the restore_object method. Object creation needs to include translation loading.