django-rest-framework serializer for ContentType object
Asked Answered
P

3

9

I am building an activity model, somewhat similar to this package. It has an actor, verb and the target.

class Activity(models.Model):
    actor_type = models.ForeignKey(ContentType, related_name='actor_type_activities')
    actor_id = models.PositiveIntegerField()
    actor = GenericForeignKey('actor_type', 'actor_id')
    verb = models.CharField(max_length=10)
    target_type = models.ForeignKey(ContentType, related_name='target_type_activities')
    target_id = models.PositiveIntegerField()
    target = GenericForeignKey('target_type', 'target_id')
    pub_date = models.DateTimeField(default=timezone.now)

Now whenever a new object of whichever models (Tender, Job and News) is created, a new Activity object is created, with the target being the objects of any of these three models.

eg. user (actor) published (verb) title (target)

class Tender(models.Model):
    title = models.CharField(max_length=256)
    description = models.TextField()

class Job(models.Model):
    title = models.CharField(max_length=256)
    qualification = models.CharField(max_length=256)

class News(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    title = models.CharField(max_length=150)

To get this data I am making an API which will get me the required json data. I am using django-rest-framework for this and very new with it.

class ActorSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'email')

class ActivitySerializer(serializers.HyperlinkedModelSerializer):
    actor = ActorSerializer()
    class Meta:
        model = Activity
        fields = ('url', 'actor', 'verb', 'pub_date')

In the above serializers, I knew that actor will be the User. And so I used the User model for the ActorSerializer class. But as for the target, it can be any of these three models (News/Job/Tender).

How can I make a serializer (eg. TargetSerialier class) for the ContentType object so that I can use the target in the ActivitySerializer class field?

Prentice answered 20/6, 2016 at 19:36 Comment(0)
P
7

Okay so answering my own question here. I had some help with zymud's answer. So, apparently in the documentation, there is a way to serialize the Generic relation.

So, all I had to do was create a custom field and associate that field in the serializer itself:

class ActivityObjectRelatedField(serializers.RelatedField):
    def to_representation(self, value):
        if isinstance(value, User):
            return 'User: ' + value.username
        elif isinstance(value, News):
            return 'News: ' + value.title
        elif isinstance(value, Job):
            return 'Job: ' + value.title
        elif isinstance(value, Tender):
            return 'Tender: ' + value.title
        raise Exception('Unexpected type of tagged object')


class ActivitySerializer(serializers.HyperlinkedModelSerializer):
    actor = ActivityObjectRelatedField(read_only=True)
    target = ActivityObjectRelatedField(read_only=True)

    class Meta:
        model = Activity
        fields = ('url', 'actor', 'verb', 'target', 'pub_date')
Prentice answered 21/6, 2016 at 6:15 Comment(1)
But does this mean that these fields are read only now? What if i'm trying to save an ActivityObject? If you don't give it a read_only, you have to give it a queryset, but I don't know what Model it's going to be before it gets there. Am I misunderstanding?Carpogonium
T
3

You can implement custom field for generic key. Example:

from django.core.urlresolvers import resolve
from rest_framework.fields import Field

class GenericRelatedField(Field):
    """
    A custom field that expect object URL as input and transforms it
    to django model instance.
    """
    read_only = False
    _default_view_name = '%(model_name)s-detail'
    lookup_field = 'pk'

    def __init__(self, related_models=(), **kwargs):
        super(GenericRelatedField, self).__init__(**kwargs)
        # related models - list of models that should be acceptable by 
        # field. Note that all this models should have corresponding 
        # endpoint.
        self.related_models = related_models

    def _get_url_basename(self, obj):
        """ Get object URL basename """
        format_kwargs = {
            'app_label': obj._meta.app_label,
            'model_name': obj._meta.object_name.lower()
        }
        return self._default_view_name % format_kwargs

    def _get_request(self):
        try:
            return self.context['request']
        except KeyError:
            raise AttributeError('GenericRelatedField have to be initialized with `request` in context')

    def to_representation(self, obj):
        """ Serializes any object to its URL representation """
        kwargs = {self.lookup_field: getattr(obj, self.lookup_field)}
        request = self._get_request()
        return request.build_absolute_uri(reverse(self._get_url_basename(obj), kwargs=kwargs))

    def clear_url(self, url):
        """ Removes domain and protocol from url """
        if url.startswith('http'):
             return '/' + url.split('/', 3)[-1]
        return url

    def get_model_from_resolve_match(self, match):
        queryset = match.func.cls.queryset
        if queryset is not None:
            return queryset.model
        else:
            return match.func.cls.model

    def instance_from_url(self, url):
        url = self.clear_url(url)
        match = resolve(url)
        model = self.get_model_from_resolve_match(match)
        return model.objects.get(**match.kwargs)


    def to_internal_value(self, data):
        """ Restores model instance from its URL """
        if not data:
            return None
        request = self._get_request()
        user = request.user
        try:
            obj = self.instance_from_url(data)
            model = obj.__class__
        except (Resolver404, AttributeError, MultipleObjectsReturned, ObjectDoesNotExist):
            raise serializers.ValidationError("Can`t restore object from url: %s" % data)
        if model not in self.related_models:
            raise serializers.ValidationError('%s object does not support such relationship' % str(obj))
        return obj

Example of usage:

class ActivitySerializer(serializers.HyperlinkedModelSerializer):
    target = GenericRelatedField(related_models=(News, Job, Tender))
    ...
Tawanda answered 20/6, 2016 at 21:0 Comment(2)
I am getting this error: Reverse for 'news-detail' with arguments '()' and keyword arguments '{'pk': 3}' not found. 0 pattern(s) tried: []Prentice
Thats because you do not have endpoint for for News model, or it basename is not news-detail. DRF model need to have endpoint to be represented as URL.Tawanda
T
3

There is a third party lib as per documentation that did the heavy lifting already:

https://www.django-rest-framework.org/api-guide/relations/#rest-framework-generic-relations

It is pretty neat actually, my serializer class ended up few readable lines:

class ActivityTypeSerializer(serializers.ModelSerializer):
    target = GenericRelatedField({
        User: UserSerializer(),
        Device: DeviceSerializer(),
    })

    class Meta:
        model = Activity
        fields = ('target', 'target_id', 'verb', 'target_ct',)
Triaxial answered 27/12, 2020 at 20:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.