Django Rest Framework and Django-Hvad #211

Closed
psychok7 opened this Issue Nov 7, 2014 · 8 comments

Comments

Projects
None yet
3 participants
@psychok7

psychok7 commented Nov 7, 2014

so i needed to add 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??

@spectras

This comment has been minimized.

Show comment
Hide comment
@spectras

spectras Nov 7, 2014

Collaborator

Hi there,
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.

Please tell me how it works for you ^^

Collaborator

spectras commented Nov 7, 2014

Hi there,
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.

Please tell me how it works for you ^^

@spectras spectras added the help label Nov 7, 2014

@spectras spectras self-assigned this Nov 7, 2014

@spectras

This comment has been minimized.

Show comment
Hide comment
@spectras

spectras Nov 8, 2014

Collaborator

Hmmm, I'd need to know more. Could you paste here:

  • the stack trace from the exception
  • the full Viewset and Serializer classes

Also, does the error show up when you create a new instance, or when you create a new translation for an existing instance? I'd guess the former but I'd better be sure.

Collaborator

spectras commented Nov 8, 2014

Hmmm, I'd need to know more. Could you paste here:

  • the stack trace from the exception
  • the full Viewset and Serializer classes

Also, does the error show up when you create a new instance, or when you create a new translation for an existing instance? I'd guess the former but I'd better be sure.

@psychok7

This comment has been minimized.

Show comment
Hide comment
@psychok7

psychok7 Nov 9, 2014

@spectras i got it to work. i was forgetting the language(), but now that i added that everything seems to be working fine.

Your code seems to be working fine. I tested it with POST, PUT and PATCH.

THanks for your help

my viewset is now like this:

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()

psychok7 commented Nov 9, 2014

@spectras i got it to work. i was forgetting the language(), but now that i added that everything seems to be working fine.

Your code seems to be working fine. I tested it with POST, PUT and PATCH.

THanks for your help

my viewset is now like this:

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()
@spectras

This comment has been minimized.

Show comment
Hide comment
@spectras

spectras Nov 9, 2014

Collaborator

Can you try changing this line?

    return Mission.objects.all()
# changes to =>
    return Mission.objects.language(user_language).all()

Actually, you should not even need to activate the translation, you should be able to strip the function to this:

    def get_queryset(self):
        user_language = self.request.GET.get('language')
        return Mission.objects.language(user_language).all()
Collaborator

spectras commented Nov 9, 2014

Can you try changing this line?

    return Mission.objects.all()
# changes to =>
    return Mission.objects.language(user_language).all()

Actually, you should not even need to activate the translation, you should be able to strip the function to this:

    def get_queryset(self):
        user_language = self.request.GET.get('language')
        return Mission.objects.language(user_language).all()
@psychok7

This comment has been minimized.

Show comment
Hide comment
@psychok7

psychok7 Nov 9, 2014

@spectras yeah that was it :) i think you can mark this as closed. thanks

psychok7 commented Nov 9, 2014

@spectras yeah that was it :) i think you can mark this as closed. thanks

@spectras

This comment has been minimized.

Show comment
Hide comment
@spectras

spectras Nov 9, 2014

Collaborator

You're welcome. Feel free to open again if needed.
Maybe someday I will make a proper package with some of the glue code I use to make hvad interact with other common apps.

Collaborator

spectras commented Nov 9, 2014

You're welcome. Feel free to open again if needed.
Maybe someday I will make a proper package with some of the glue code I use to make hvad interact with other common apps.

@alfredrumss

This comment has been minimized.

Show comment
Hide comment
@alfredrumss

alfredrumss Aug 16, 2017

hi @spectras

i have a similiar error but when I implement a filter class on my ViewSet:

class RestaurantViewSet(viewsets.ModelViewSet):
queryset = Restaurant.objects.language().all()
serializer_class = RestaurantSerializer
permission_classes = (permissions.AllowAny, )
filter_backends = (filters.DjangoFilterBackend,)
filter_class = RestaurantFilters
lookup_field = 'slug'
pagination_class = StandardResultsSetPagination

def get_queryset(self):
	user_language = self.request.GET.get('language')
	return Restaurant.objects.language(user_language).all().order_by('-created_at')

My filter class is:

class RestaurantFilters(FilterSet):
category = CharFilter(name='category', lookup_expr='exact')

class Meta:
	model = Restaurant
	fields = ['category']

alfredrumss commented Aug 16, 2017

hi @spectras

i have a similiar error but when I implement a filter class on my ViewSet:

class RestaurantViewSet(viewsets.ModelViewSet):
queryset = Restaurant.objects.language().all()
serializer_class = RestaurantSerializer
permission_classes = (permissions.AllowAny, )
filter_backends = (filters.DjangoFilterBackend,)
filter_class = RestaurantFilters
lookup_field = 'slug'
pagination_class = StandardResultsSetPagination

def get_queryset(self):
	user_language = self.request.GET.get('language')
	return Restaurant.objects.language(user_language).all().order_by('-created_at')

My filter class is:

class RestaurantFilters(FilterSet):
category = CharFilter(name='category', lookup_expr='exact')

class Meta:
	model = Restaurant
	fields = ['category']
@alfredrumss

This comment has been minimized.

Show comment
Hide comment
@alfredrumss

alfredrumss Aug 16, 2017

the error is

FilterSet model <class 'restaurant.models.Restaurant'> does not match queryset model <class 'restaurant.models.RestaurantTranslation'>

I suppose that is the filter language that I must to include on my filter class, but i don't any idea..

alfredrumss commented Aug 16, 2017

the error is

FilterSet model <class 'restaurant.models.Restaurant'> does not match queryset model <class 'restaurant.models.RestaurantTranslation'>

I suppose that is the filter language that I must to include on my filter class, but i don't any idea..

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