DRF serializer get liked data with post by request user - django

I'm making a social app like facebook.
when getting post(at news feed) data I would like to get Boolean if I pressed like about that post.
models.py
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Post(models.Model):
uploader = models.ForeignKey(User)
likes = models.IntegerField(default=0)
point = models.IntegerField(default=0)
isPointReceived = models.BooleanField(default=False)
content = models.TextField()
uploadedTime = models.DateTimeField(auto_now=True)
# editedTime = models.DateTimeField()
def __str__(self):
return ("[uploader = " + self.uploader.username + "]") + (", [id = " + (str)(self.id) + "]") + ("[content = " + self.content[:50] + "]")
class PostLike(models.Model):
post = models.ForeignKey(Post, related_name='postLikes')
liker = models.ForeignKey(User)
def __str__(self):
return "Like" + "| [Post = " + (str)(self.post) + "]" + ", [Liker = " + self.liker.username + "]"
serializers.py
class PostLikeSerializer(serializers.ModelSerializer):
class Meta:
model = PostLike
fields = '__all__'
class PostListSerializer(serializers.ModelSerializer):
uploader = UserDetailSerializer()
isMine = serializers.SerializerMethodField()
isLiked = serializers.SerializerMethodField()
postComments = PostCommentSerializer(many=True, allow_null=True)
class Meta:
model = Post
fields = ('uploader', 'id', 'likes', 'point', 'isPointReceived', 'content', 'uploadedTime', 'postComments', 'isMine', 'isLiked',)
def get_isMine(self, obj):
requestUser = CurrentUserDefault()
return obj.objects.fileter(uploader=requestUser).exists()
# return obj.filter(uploader=requestUser)
def get_isLiked(self, obj):
requestUser = CurrentUserDefault()
return PostLike.objects.filter(post=obj, liker=requestUser).exists()
#
# try:
# PostLike.objects.get(post=obj, liker=requestUser)
# return True
# except:
# return False
I tried lot of dirty stuffs. but there was no solution..
conclusion:
HTF to get data about, if user has a record of PostLike(post=post, user=user)
how to get user in serializer Class.
or should I approach this in different ways?
like managing liked data in APIView or whatever etc...
help!
========EDITED==========
final code should look like this.
in serialziers.py
class PostListSerializer(serializers.ModelSerializer):
uploader = UserDetailSerializer()
postComments = PostCommentSerializer(many=True, allow_null=True)
postLikes = PostLikeSerializer(many=True, allow_null=True)
postImages = PostImageSerializer(many=True, allow_null=True)
isMine = serializers.SerializerMethodField()
isLiked = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ('uploader', 'id', 'likes', 'content', 'uploadedTime', 'postComments', 'postLikes', 'postImages', 'isMine', 'isLiked',)
def get_isMine(self, obj):
requestUser = self.context['request'].user
return obj.uploader == requestUser
def get_isLiked(self, obj):
requestUser = self.context['request'].user
return PostLike.objects.filter(post=obj, liker=requestUser).exists()

You can get a user from serializer context inside serializer method:
self.context['request'].user
It passed from a method get_serializer_context which originally created in a GenericAPIView:
class GenericAPIView(APIView):
....
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
As you can see you could also get format and view from context. You could also override this method for set additional context.

def get_isLiked(self, obj):
requestUser = self.context['request'].user
return PostLike.objects.filter(post=obj, liker=requestUser).exists()
If results have 12 objects it will execute 12 similar query, which increase increase your response time. I am looking for solution so that I can make it with one query. Don't know how can I do that.

def get_isLiked(self, obj):
requestUser = self.context['request'].user
return PostLike.objects.filter(post=obj, liker=requestUser).exists()
You should to get all PostLike fields in cache and use prefetch_related method in your query set. This thing will check all entries using cache, not multiple queries.
For example:
def get_isLiked(self, obj):
requestUser = self.context['request'].user
likes = PostLike.objects.all()
return likes.filter(post=obj, liker=requestUser).exists()
and somewhere in models or views or managers in "get_queryset" you should add:
queryset.prefetch_related('postlike')

Related

what is the correct way to automatically set a ForeignKey(User, on_delete=models.CASCADE) to the current loging in user

i need to auto set the model field "auther = models.ForeignKey(User, on_delete=models.CASCADE)" to the current authenticated user so that any posts created are automatically assigned to the user that created them...
currently with my code the user has to select their own username in a drop down list. i would like to remove this and have that field auto filled out in the back end to prevent users picking the wrong username.
here is my models.py
class Task(models.Model):
SELECT = 'None'
GREEN = 'Green'
AMBER = 'Amber'
RED = 'Red'
PRIORITY = [(SELECT, 'Select Priority'),(GREEN, 'Green'),
(AMBER, 'Amber'),(RED, 'Red'),]
auther = models.ForeignKey(User, on_delete=models.CASCADE,)
priority = models.CharField(max_length=5,choices=PRIORITY,default=SELECT,)
date = models.DateField(auto_now_add=True)
title = models.CharField(max_length=255)
description = models.TextField()
def __str__(self):
return self.title + ' | ' + str(self.auther)
def get_absolute_url(self):
return reverse('task')
def is_upperclass(self):
return self.PRIORITY in {self.GREEN, self.AMBER}
here is my views.py
class TaskView(ListView):
model = Task
template_name = 'tasks.html'
ordering = ['-id']
class AddTaskView(SuccessMessageMixin,CreateView):
model = Task
template_name = 'add_task.html'
fields = ['priority','title','description']
success_message = " Task was created successfully"
class UpdateTaskView(SuccessMessageMixin, UpdateView):
model = Task
template_name = 'update_task.html'
fields = ['priority','title','description']
success_message = " Task was updated successfully"
class DeleteTaskView(SuccessMessageMixin, DeleteView):
model = Task
template_name = 'delete_task.html'
success_url = '/task/'
success_message = "Task was closed successfully"
ive tried adding my own save_model method but this didnt work as i get the error "IntegrityError at /add_task/
NOT NULL constraint failed: home_task.auther_id"
when i remove the option to pick the user in the form...
that attempt looks like this
class Task(models.Model):
SELECT = 'None'
GREEN = 'Green'
AMBER = 'Amber'
RED = 'Red'
PRIORITY = [(SELECT, 'Select Priority'),(GREEN, 'Green'),
(AMBER, 'Amber'),(RED, 'Red'),]
auther = models.ForeignKey(User, on_delete=models.CASCADE,)
priority = models.CharField(max_length=5,choices=PRIORITY,default=SELECT,)
date = models.DateField(auto_now_add=True)
title = models.CharField(max_length=255)
description = models.TextField()
def __str__(self):
return self.title + ' | ' + str(self.auther)
def get_absolute_url(self):
return reverse('task')
def is_upperclass(self):
return self.PRIORITY in {self.GREEN, self.AMBER}
def save_model(self, request, obj, form, change):
obj.auther = request.user
super().save_model(request, obj, form, change)
In the CreateView, you assign it to the instance:
from django.contrib.auth.mixins import LoginRequiredMixin
class AddTaskView(SuccessMessageMixin, LoginRequiredMixin, CreateView):
model = Task
template_name = 'add_task.html'
fields = ['priority', 'title', 'description']
success_message = " Task was created successfully"
def form_valid(self, form):
form.instance.auther = self.request.user
return super().form_valid(form)
Note: It is normally better to make use of the settings.AUTH_USER_MODELĀ [Django-doc] to refer to the user model, than to use the User modelĀ [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.

Django rest framework get or create for PrimaryKeyRelatedField

I start to create REST API for my web-application with Django and Django rest framework and I need one logic problem.
There are entities Instruction and Tag. The user visit my service and create self Instruction and add exists Tag OR new Tag for it.
I created my model seriallizer class with using PrimaryKeyRelatedField for relation Instruction<->Tag. But if I do POST for a new Instruction with new Tag I got error: "Invalid pk \"tagname\" - object does not exist.".
I solved this problem with the overriding of the to_internal_value method in my field class.
What is the best practice for solving this problem? It seems to me this problem is typical for web and REST API.
My models:
class Tag(Model):
name = CharField(max_length=32, verbose_name=_("Name"),
unique=True, validators=[alphanumeric], primary_key=True)
def __str__(self):
return self.name
class Instruction(Model):
user = ForeignKey(settings.AUTH_USER_MODEL,
related_name='instructions',
on_delete=CASCADE,
blank=False, null=False,
verbose_name=_("User"))
title = CharField(max_length=256,
verbose_name=_("Title"),
blank=False, null=False)
created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
modified_datetime = DateTimeField(
verbose_name=_("Last modification time"), blank=False, null=False)
tags = ManyToManyField(Tag,
related_name="instructions",
verbose_name=_("Tags"))
class Meta:
ordering = ['-created_datetime']
# singular_name = _("")
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
n = now()
if self.id is None:
self.created_datetime = n
self.modified_datetime = n
super(Instruction, self).save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.title
my serializers:
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('name',)
class InstructionSerializer(serializers.ModelSerializer):
tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
author = serializers.SerializerMethodField()
def get_author(self, obj):
return obj.user.username
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author')
read_only_fields = ('modified_datetime',)
I created new field class class PrimaryKeyCreateRelatedField and overrided to_internal_value method for creating the new Tag object instead raising with message 'does_not_exist':
PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
# self.fail('does_not_exist', pk_value=data)
return self.get_queryset().create(pk=data)
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
my view:
class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
serializer_class = InstructionSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def create(self, request, *args, **kwargs):
data = dict.copy(request.data)
data['user'] = self.request.user.pk
serializer = InstructionSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Update
models.py
alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$',
_('Only alphanumeric characters are allowed.'))
class Tag(Model):
name = CharField(max_length=32, verbose_name=_("Name"),
unique=True, validators=[alphanumeric], primary_key=True)
def __str__(self):
return self.name
class Step(PolymorphicModel):
instruction = ForeignKey(Instruction,
verbose_name=_("Instruction"),
related_name='steps',
blank=False, null=False,
on_delete=CASCADE)
position = PositiveSmallIntegerField(verbose_name=_("Position"), default=0)
description = TextField(verbose_name=_("Description"),
max_length=2048,
blank=False, null=False)
class Meta:
verbose_name = _("Step")
verbose_name_plural = _("Steps")
ordering = ('position',)
unique_together = ("instruction", "position")
def __str__(self):
return self.description[:100]
class Instruction(Model):
user = ForeignKey(settings.AUTH_USER_MODEL,
related_name='instructions',
on_delete=CASCADE,
blank=False, null=False,
verbose_name=_("User"))
title = CharField(max_length=256,
verbose_name=_("Title"),
blank=False, null=False)
created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
modified_datetime = DateTimeField(
verbose_name=_("Last modification time"), blank=False, null=False)
tags = ManyToManyField(Tag,
related_name="instructions",
verbose_name=_("Tags"))
# thumbnail = #TODO: image field
class Meta:
ordering = ['-created_datetime']
# singular_name = _("")
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
n = now()
if self.id is None:
self.created_datetime = n
self.modified_datetime = n
super(Instruction, self).save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.title
views.py
class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_serializer_class(self):
"""Return different serializer class for different action."""
if self.action == 'list':
return InstructionSerializer
elif self.action == 'create':
return InstructionCreateSerializer
serialiers.py
class PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
# self.fail('does_not_exist', pk_value=data)
return self.get_queryset().create(pk=data)
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
class InstructionCreateSerializer(serializers.ModelSerializer):
tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
steps = InstructionStepSerializer(many=True)
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'steps')
read_only_fields = ('modified_datetime',)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
steps_data = validated_data.pop('steps')
# NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
# "needs to have a value for field "id" before this many-to-many relationship can be used."
instruction = Instruction.objects.create(**validated_data)
for tag in tags_data:
instruction.tags.add(tag)
for step in steps_data:
Step.objects.create(instruction=instruction,
description=step['description'],
position=step['position'])
return instruction
class InstructionSerializer(serializers.ModelSerializer):
tags = serializers.StringRelatedField(many=True)
author = serializers.SerializerMethodField()
steps = InstructionStepSerializer(many=True)
def get_author(self, obj):
return obj.user.username
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author', 'steps')
read_only_fields = ('modified_datetime',)
In my case to solve the problem I need to override the method run_validation. That allow make check of tags and create their (if not exists) before validation.
class InstructionCreateSerializer(serializers.ModelSerializer):
steps = InstructionStepSerializer(many=True)
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Instruction
fields = ('title', 'created_datetime', 'modified_datetime', 'tags', 'steps', 'id', 'user')
read_only_fields = ('modified_datetime',)
def run_validation(self, data=serializers.empty):
if 'tags' in data:
for tag in data['tags']:
Tag.objects.get_or_create(name=tag)
return super(InstructionCreateSerializer, self).run_validation(data)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
steps_data = validated_data.pop('steps')
# NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
# "needs to have a value for field "id" before this many-to-many relationship can be used."
instruction = Instruction.objects.create(**validated_data)
for tag in tags_data:
instruction.tags.add(tag)
for step in steps_data:
Step.objects.create(instruction=instruction,
description=step['description'],
position=step['position'])
return instruction
Apart from the answers given by #YPCrumble and #SijanBhandari, I just had to comment on something in your code.
In the models.py, you have overridden the save method for adding created_at and modified_on. For that you could just add
created_at = models.DateTimeField(auto_now_add=True)
modified_on = DateTimeField (auto_now=True)
The auto_now_add option sets when the object is created for the first time.
It's not editable. The auto_now setting sets whenever the object is saved, ie, whenever object.save() method is called upon.
These usually are used for timestamping the objects for future references.
Why write so many lines, when you could do this on just 2 lines of code.
Just a heads up though!!
For further details, go to the documentation here
In "regular" Django you usually want to create your model instance in the form's save method, not the view. DRF is similar, in that you want to create your model instances in the serializer's create or update methods. The reason for this is that if you need to add a new endpoint to your API you can reuse the serializer and would not have to write duplicate code creating or updating your model instance.
Here's how I'd refactor your code:
Remove the entire create method from your ModelViewSet - you don't need to override that.
Remove the custom PrimaryKeyCreateRelatedField - you just need a PrimaryKeyRelatedField
Add two methods to your serializer - create and update:
In the create method, create your tag objects before saving the instruction object like you can see in the DRF docs. You can get the current user like you were doing in your view via self.context['request'].user in this create method. So you might create the Instruction like Instruction.objects.create(user=self.context['request'].user, **validated_data) and then loop through the tags (like they do for tracks in the docs) to add them to the Instruction.
The docs don't have an example update method but essentially your update method also takes an instance parameter for the existing instruction. See this answer from the creator of DRF for more details
The best way would be sort out everything at your CREATE method of the view.
I believe you tags will be sent from your front-end to the back-end at the format of
[ 1,
{'name': "TEST"},
{'name': 'TEST2'}
]
Here '1' is the existing tag id and 'TEST' and 'TEST2' are the two new tags inserted by
the user. Now you can change your CREATE method as follows:
class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
serializer_class = InstructionSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def create(self, request, *args, **kwargs):
data = dict.copy(request.data)
data['user'] = self.request.user.pk
# MODIFICATION.....
tags = self.request.get('tags', None)
tag_list = []
if tags:
for tag in tags:
if isinstance(tag, dict):
new_tag = Tag.objects.create(name=tag['name'])
tag_list.append(new_tag.id)
else:
tag_list.append(int(tag))
data = {
'title': ....
'tags': tag_list,
'user': ...
'author': ...
......
}
serializer = InstructionSerializer(data=data)
I hope it will be helpful for you.

How do I add a field to a django-rest-framework serializer that isn't on my model?

I have a serializer that works fine for the GET, POST, DELETE actions. It exposes the model fields that I want. However for the PUT action, the user will send back values that aren't built into my models and the server will deal with how to perform the update on the model. I can send the data back using Postman or Curl and it works but the browseable API still looks like this:
For the PUT method I want "is_winner", "num_hands_won", and "score" to show up instead of the actual model fields. How do I do this? (Let me know in the comments if you need more info)
StatisticsSerializer:
class StatisticsSerializer(serializers.ModelSerializer):
# pk = serializers.IntegerField(required=False)
class Meta:
model = Statistics
fields = [
'url',
'games_won',
'hands_won',
'games_played',
'high_score',
'low_score',
]
Statistics Model:
class Statistics(models.Model):
# Define model fields:
user = models.OneToOneField(User, primary_key=True)
games_won = models.IntegerField(null=True, blank=True)
hands_won = models.IntegerField(null=True, blank=True)
games_played = models.IntegerField(null=True, blank=True)
high_score = models.IntegerField(null=True, blank=True)
low_score = models.IntegerField(null=True, blank=True)
def __str__(self):
return str(self.pk)
def increment_games_won(self, is_winner):
if is_winner is True:
self.games_won = self.games_won + 1
return self.games_won
def add_to_hands_won(self, num_hands_won):
if num_hands_won > 0 and num_hands_won < 8:
self.hands_won = self.hands_won + num_hands_won
return self.hands_won
def increment_games_played(self):
self.games_played = self.games_played + 1
return self.games_played
def new_high_score(self, score):
if score > self.high_score:
self.high_score = score
return self.high_score
def new_low_score(self, score):
if score < self.low_score:
self.low_score = score
return self.low_score
Statistics ViewSet:
class StatisticsViewSet(DefaultsMixin, viewsets.ModelViewSet):
queryset = Statistics.objects.all()
serializer_class = StatisticsSerializer
filter_class = StatisticsFilter
search_fields = ('pk', 'user')
ordering_fields = ('games_won', 'hands_won', 'games_played', 'high_score', 'low_score')
def update(self, request, pk=None):
stats = self.get_object()
stats.increment_games_won(request.data['is_winner'])
stats.add_to_hands_won(request.data['num_hands_won'])
stats.increment_games_played()
stats.new_low_score(request.data['score'])
stats.new_high_score(request.data['score'])
stats.save()
serialized_stats = StatisticsSerializer(stats, context={'request': request}).data
return Response(serialized_stats)
You could probably use another Serializer and use it for you PUT API
StatisticsUpdateSerializer:
class StatisticsUpdateSerializer:
is_winner = ...
num_hands_won = ...
score = ...
And use this serializer in the PUT API or create a new route as shown in the example mentioned in the DRF documentation here
#detail_route(methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
// Use your serializer below
serializer = PasswordSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.data['password'])
user.save()
return Response({'status': 'password set'})
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)

Update if exists or create Model in Django Rest

I got this model:
class Like(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
post = models.ForeignKey('posts.Post', blank=True, null=True)
RATING_CONVERSION = (
(1, '+'),
(0, '0'),
(-1, '-'),
)
userRating = models.SmallIntegerField(choices=RATING_CONVERSION)
def __int__(self):
return self.id
If post id and user id exists I need to update rating.
I try to make it.
Serializer:
class LikeSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(queryset=ExtUser.objects.all(), required=False, allow_null=True,default=None)
class Meta:
model = Like
field = ('user', 'post')
validators = [
UniqueTogetherValidator(
queryset=Like.objects.all(),
fields=('user', 'post')
)
]
def validate_user(self, value):
return self.context['request'].user
def create(self, validated_data):
return Like.objects.create(**validated_data)
And ViewSet
class LikeViewSet(viewsets.ModelViewSet):
queryset = Like.objects.all()
serializer_class = LikeSerializer
#detail_route(methods=['post', 'get'])
def get_object(self):
if self.request.method == 'POST':
like = Like.objects.get(user=self.context['request'].user, post = self.context['request'].post)
if like:
return like
else:
return Like(id=self.kwargs.get('pk'))
else:
return super(LikeViewSet, self).get_object()
I found a lot of information how to create or update using pk in Url, but I got parameters inside of JSON
This method doesn't work - instead of update rating it creates new Model objects

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?
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')
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))
...
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',)