Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django Rest Framework, HyperlinkedModelSerializers, ModelViewSets, and writable GenericForeignKeys: how?

I've got a model FinancialTransaction which has the typical content_type, object_id, and content_object fields to set up generic relations to any of my other models.

I've figured out how to serialize this relation for reading:

class FinancialTransactionSerializer(serializers.HyperlinkedModelSerializer):
    content_object = serializers.SerializerMethodField('get_content_obj_url')

    def get_content_obj_url(self, obj):
        obj = obj.content_object

        view_name = obj._meta.object_name.lower() + "-detail"
        s = serializers.HyperlinkedIdentityField(source=obj, view_name=view_name)
        s.initialize(self, None)
        return s.field_to_native(obj, None)

    class Meta:
        model = FinancialTransaction
        fields = ('id', 'value', 'date', 'memo', 'banking_account', 'content_object')

The ViewSet:

class FinancialTransactionViewSet(viewsets.ModelViewSet):
    model = FinancialTransaction
    serializer_class = FinancialTransactionSerializer

This creates a hyperlink to the related object for the serialized representation when I do a GET on the view.

However, I'm kind of stuck on how to make it so that I can POST a new FinancialTransaction with an already existing related object.

Ideally, it would work just like a normal ForeignKey where I can POST something like:

{"value": "200.00",
 "date": "2014-10-10",
 "memo": "repairs",
 "banking_account": "http://domain.com/api/banking_account/134/",
 "content_object": "http://domain.com/api/property/432/"
}
like image 295
Dustin Wyatt Avatar asked May 29 '26 18:05

Dustin Wyatt


1 Answers

Ok, to answer my own question...

I overrode restore_fields in my own serializer like this:

class FinancialTransactionSerializer(serializers.HyperlinkedModelSerializer):
    content_object = serializers.SerializerMethodField('get_content_obj_url')

    def get_content_obj_url(self, obj):
        obj = obj.content_object

        view_name = get_view_name(obj)
        s = serializers.HyperlinkedIdentityField(source=obj, view_name=view_name)
        s.initialize(self, None)
        return s.field_to_native(obj, None)

    def restore_fields(self, data, files):
        content_object = None

        if 'content_object' in data:
            request = self.context.get('request')  
            content_object = get_object_from_url(request.DATA['content_object'])

        attrs = super(FinancialTransactionSerializer, self).restore_fields(data, files)
        if content_object:
            attrs['content_object'] = content_object
        return attrs

    class Meta:
        model = FinancialTransaction
        fields = ('id', 'value', 'date', 'memo', 'banking_account', 'content_object')

def get_model_from_url(url: str):
    return resolve(urlparse(url).path).func.cls.model

def get_object_from_url(url: str):
    model = get_model_from_url(url)
    pk = resolve(urlparse(url).path).kwargs.get('pk')
    if not pk:
        return None
    return model.objects.get(pk=pk)

This setup serializes objects so that the content_object field contains a hyperlink to the related object, and when POST'ing to a view using this serializer, and the data includes the content_object key, we get the related object and pass it on.

The attrs returned from restore_fields is used in the restore_object method, and since we looked up the content object and put it in attrs, restore_object sets the content_object attribute on the FinancialTransaction object to the retrieved object and then Django takes care of the rest.

So far the only downside I can see is that this doesn't add the content_object field to the browsable API...but I'm not sure how that'd work anyway since related objects are usually provided in a select, and I don't think we'd want a select populated with every single object in our database.

like image 98
Dustin Wyatt Avatar answered May 31 '26 09:05

Dustin Wyatt