5
class Foo(models.Model):
    bar = models.CharField(max_length=300)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')


class FooSerializer(serializers.ModelSerializer):
    class Meta:
       model = Foo

class FooViewSet(viewsets.ModelViewSet):
    model = Foo
    serializer_class = FooSerializer

I can now post data to the viewset that looks like this:

{
    bar: 'content',
    content_type: 1
    object_id: 5
}

The only thing that's bugging me is that the frontend would have to be aware of the contenttype id's

Instead I want to be able to post the content_types name like 'User' as content_type and have the backend determine the id.

Nilesh
  • 20,521
  • 16
  • 92
  • 148
matteok
  • 2,189
  • 3
  • 30
  • 54

3 Answers3

11

The easiest and cleanest method as of DRF 3.x for read/write operations:

from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
from .models import Foo

class FooSerializer(serializers.ModelSerializer):
    class Meta:
       model = Foo

    content_type = serializers.SlugRelatedField(
        queryset=ContentType.objects.all(),
        slug_field='model',
    )

You can then perform CRUD operations using the model name:

data = {
    'bar': "content",
    'content_type': "model_name",
    'object_id': 1,
}
DarkFranX
  • 361
  • 2
  • 13
  • I think this is better - because the caller of endpoints might not capable to handle generic type. By returning the ID and type (in string), the caller can further requests the content via appropriate endpoints. – John Pang Sep 26 '18 at 12:35
8

You could customize WritableField to map contenttype id to 'app_label.model' string:

class ContentTypeField(serializers.WritableField):
    def field_from_native(self, data, files, field_name, into):
        into[field_name] = self.from_native(data[field_name])

    def from_native(self, data):
        app_label, model = data.split('.')
        return ContentType.objects.get(app_label=app_label, model=model)

    # If content_type is write_only, there is no need to have field_to_native here.
    def field_to_native(self, obj, field_name):
        if self.write_only:
            return None
        if obj is None:
            return self.empty
        ct = getattr(obj, field_name)
        return '.'.join(ct.natural_key())


class FooSerializer(serializers.ModelSerializer):
    content_type = ContentTypeField()
    # ...

You may want to do a second mapping to limit choices of contenttype and to avoid unveiling of your app/model names:

CONTENT_TYPES = {
  'exposed-contenttype': 'app_label.model'
}

class ContentTypeField(...):
    def from_native(self, data):
        if data not in CONTENT_TYPES:
            raise serializers.ValidationError(...)
        app_label, model = CONTENT_TYPES[data].split('.')
        # ...
okm
  • 23,575
  • 5
  • 83
  • 90
2

DRF has changed and now instead of from_native and field_to_native there are new methods - to_internal_value and to_representation.

it's even simpler now:

class ContentTypeField(serializers.Field):

    def to_representation(self, obj):
        return obj.model

    def to_internal_value(self, data):
        return ContentType.objects.get(model=data)
Rani
  • 6,424
  • 1
  • 23
  • 31