Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

django-rest-framework serializer for ContentType object

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?

like image 564
Aamu Avatar asked Jun 20 '16 19:06

Aamu


People also ask

Do we need Serializers in Django REST Framework?

Serializers in Django REST Framework are responsible for converting objects into data types understandable by javascript and front-end frameworks. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.

How do you pass extra context data to Serializers in Django REST Framework?

In function based views we can pass extra context to serializer with "context" parameter with a dictionary. To access the extra context data inside the serializer we can simply access it with "self. context". From example, to get "exclude_email_list" we just used code 'exclude_email_list = self.

What is the difference between ModelSerializer and HyperlinkedModelSerializer?

The HyperlinkedModelSerializer class is similar to the ModelSerializer class except that it uses hyperlinks to represent relationships, rather than primary keys. By default the serializer will include a url field instead of a primary key field.


2 Answers

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')
like image 120
Aamu Avatar answered Oct 19 '22 13:10

Aamu


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))
    ...
like image 37
zymud Avatar answered Oct 19 '22 12:10

zymud