DRF add computed field based on requesting user - python

I have a model which has specific many to many fields to the user model. Now, to prevent information leaking, I do not want to return the whole related field though the rest framework. But, I want to create some kind of computed field, such that it return True if the requesting user is in the related field, and False otherwise. Is there a way to make this work?
For example, as it stands now, the rest framework will list the users for "user_like" and
"user_bookmark", which I dont want to happen, hence I want to exclude them from the serialized. But I want to have a field, say, named is_liked, which will be true if request.user is in user_like, and false otherwise.
My current setup:
model
class Post(models.Model):
title = models.CharField(max_length=100)
user = models.ForeignKey(User, on_delete=models.CASCADE)
image = models.ImageField(upload_to='dream_photos')
description = models.TextField(max_length=500)
date_added = models.DateTimeField(auto_now_add=True)
user_like = models.ManyToManyField(User, related_name='likes', blank=True)
user_bookmark = models.ManyToManyField(
User, related_name='bookmarks', blank=True)
total_likes = models.PositiveIntegerField(db_index=True, default=0)
tags = TaggableManager()
serialiser
class PostSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField()
class Meta:
model = Dream
fields = ('title','user', 'image','description','date_added', 'tags', 'total_likes' )
views
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.prefetch_related('user').all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=False, methods=['get'], url_path='current-profile', url_name='current-profile')
def current_user_posts(self, request):
# I expected this to add the extra field I required
# But it does not seem to work as expected
queryset = self.get_queryset().filter(user=request.user).annotate(
bookmark=(request.user in "user_bookmark"))
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
Expected behavior when requesting:
{
"id": 1,
"tags": [
"test"
],
"title": "Tets",
"image": "http://127.0.0.1:8000/media/post_photos/photo1648638314.jpeg",
"description": "TEst",
"date_added": "2022-05-20T17:47:55.739431Z",
"total_likes": 0,
"user": 1,
"like": true, // true if current user is in user_like, false otherwise
"bookmark": true // true if current user is in user_bookmark, false otherwise
}
Actual behavior
TypeError: 'in ' requires string as left operand, not SimpleLazyObject
Edit 1:
The answer from here seems to help to resolve the error. Unfortunately, the annotated field does not seem to be returned by the serializer
the edited view:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.prefetch_related('user').all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=False, methods=['get'], url_path='current-profile', url_name='current-profile')
def current_user_posts(self, request):
queryset = self.get_queryset().filter(user=request.user).annotate(
bookmark=Exists(Post.user_bookmark.through.objects.filter(
post_id=OuterRef('pk'), user_id=request.user.id))
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

What you can do is add your custom fields to the serializer with SerializerMethodField and pass the request.user via get_serializer_context in your view. For example:
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.prefetch_related('user').all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=False, methods=['get'], url_path='current-profile', url_name='current-profile')
def current_user_posts(self, request):
queryset = self.get_queryset().filter(user=request.user)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_serializer_context(self):
context = super(PostViewSet, self).get_serializer_context()
context.update({"request": self.request})
return context
This allows you to sent the request via the context which can be used by the serializer. Now in your serializer you can add this two new fields:
class PostSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField()
bookmark = serializers.SerializerMethodField()
like = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ('title','user', 'image','description','date_added', 'tags', 'total_likes', 'bookmark', 'like')
def get_like(self, obj):
return self.context['request'].user in obj.user_like.all()
def get_bookmark(self, obj):
return self.context['request'].user in obj.user_bookmark.all()

Related

Django Rest Framework fail on setting a new context to the serializer

Django time:
I am facing an issue with providing a context to the serializer:
class CommentSerializer(serializers.ModelSerializer):
likes = CustomUserSerializer(many=True,source='likes.all')
class Meta:
fields = 'likes',
model = models.Comment
def get_user_like(self,obj):
for i in obj.likes.all():
if self.context['user'] in i.values():
return self.context['user']
in the view:
class CommentView(viewsets.ModelViewSet):
serializer_class = serializer.CommentSerializer
def get_serializer_context(self): #adding request.user as an extra context
context = super(CommentView,self).get_serializer_context()
context.update({'user':self.request.user})
return context
as you can see, i have overridded get_serializer_context to add user as a context
however, in the serializer side, i am getting KeyError:'user' means the key does not exist, any idea how to set a context?
This is not necessary and inefficient. You can just annotate with:
from django.db.models import Exists, OuterRef
class CommentView(viewsets.ModelViewSet):
serializer_class = serializer.CommentSerializer
def get_queryset(self):
return Comment.objects.annotate(
user_like=Exists(
Like.objects.filter(
comment_id=OuterRef('pk'), user_id=self.request.user.pk
)
)
).prefetch_related('likes')
In the serializer we then add the user_like field:
class CommentSerializer(serializers.ModelSerializer):
likes = CustomUserSerializer(many=True)
user_like = serializers.BooleanField(read_only=True)
class Meta:
fields = ('likes',)
model = models.Comment

Search fields in custom action method

now i'm studying DRF and have to do project with photo albums. One of my tasks is to create custom #action "patch" method, using model field "title", but i can't understand how to add fields for search in custom methods. We can see them in base methods, like "get", "patch" and "put", but i can't find any info about how to add them to custom actions.
If anyone knows how to do it, please, tell me.
My model:
class PhotoAlbum(models.Model):
title = models.CharField(verbose_name='Название альбома', max_length=50, null=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name='Автор')
created_at = models.DateTimeField(verbose_name='Дата создания', editable=False,
default=datetime.datetime.today())
class Meta:
verbose_name = 'Фотоальбом'
verbose_name_plural = 'Фотоальбомы'
def __str__(self):
return f'{self.title} Автор: {self.created_by} Дата: {self.created_at}'
My view:
def photo_album_view(request):
photo_albums = PhotoAlbum.objects.all()
context = {
'photo_albums': photo_albums,
}
return render(request, 'photo_album.html', context=context)
My viewset:
class AlbumFilter(django_filters.FilterSet):
title = django_filters.Filter(field_name='title')
class PhotoAlbumViewSet(viewsets.ModelViewSet):
queryset = PhotoAlbum.objects.all()
filterset_class = AlbumFilter
serializer_class = PhotoAlbumSerializer
pagination_class = ResultPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['title', ]
ordering_fields = ['created_at', ]
To answer the above
say you have a viewset class
// all imports
class AbcApi(viewsets.ModelViewset):
serializer_class = random
permission_classses = [IsAuthenticated]
search_fields = ["a_field", "b_field"]
filter_backends = [....]
#custom action
#action(detail=False)
def custom_action(self, *args, **kwargs):
""" to answer your question """
qset_ = **self.filter_queryset**(self.queryset())
#the filter bold automatically involves the class filters in the custom method
the answer is to use self.filter_queryset(..pass the get_queryset()) here as seen
If you are using ModelViewSet, I believe you are looking for one of these custom methods:
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
These functions allow you to add custom functionalities to your update method. Reference
Brief example:
def partial_update(self, request, pk=None):
serializer = UserPostSerializer(user, data=request.data, partial=True)
if serializer.is_valid():
try:
serializer.save()
except ValueError:
return Response({"detail": "Serializer not valid."}, status=400)
return Response({"detail": "User updated."})
else:
return Response(serializer.errors)

DRF: how to change the value of the model fields before saving to the database

If I need to change some field values before saving to the database as I think models method clear() is suitable. But I can't call him despite all my efforts.
For example fields email I need set to lowercase and fields nda I need set as null
models.py
class Vendors(models.Model):
nda = models.DateField(blank=True, null=True)
parent = models.OneToOneField('Vendors', models.DO_NOTHING, blank=True, null=True)
def clean(self):
if self.nda == "":
self.nda = None
class VendorContacts(models.Model):
....
vendor = models.ForeignKey('Vendors', related_name='contacts', on_delete=models.CASCADE)
email = models.CharField(max_length=80, blank=True, null=True, unique=True)
def clean(self):
if self.email:
self.email = self.email.lower()
serializer.py
class VendorContactSerializer(serializers.ModelSerializer):
class Meta:
model = VendorContacts
fields = (
...
'email',)
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors.objects.create(**validated_data)
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
views.py
class VendorsCreateView(APIView):
"""Create new vendor instances from form"""
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsSerializer
def post(self, request, *args, **kwargs):
serializer = VendorsSerializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
else:
return Response(request.data, status=status.HTTP_200_OK)
As I learned from the documentation
Django Rest Framework serializers do not call the Model.clean when
validating model serializers
In dealing with this problem, I found two ways to solve it.
1. using the custom method at serializer. For my case, it looks like
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors.objects.create(**validated_data)
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
def validate(self, attrs):
instance = Vendors(**attrs)
instance.clean()
return attrs
Using full_clean() method. For me, it looks like
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors(**validated_data)
vendor.full_clean()
vendor.save()
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
But in both cases, the clean() method is not called. I really don't understand what I'm doing wrong.
In my case I had the same problem but with validation feature
I used the way below and it works for me (not excludes the way found above):
class CustomViewClass(APIView):
def post(self, request, format=None):
prepared_data_variable = 'some data in needed format'
serializer = CustomSerializer(data=request.data)
if serializer.is_valid(self):
serializer.validated_data['field_name'] = prepared_data_variable
serializer.save()
return Response(data=serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
This string is key for my solution serializer.validated_data['field_name'] = prepared_data_variable
For DRF you can change your serializer before save as below...
First of all, you should check that serializer is valid or not, and if it is valid then change the required object of the serializer and then save that serializer.
if serializer.is_valid():
serializer.object.user_id = 15 # For example
serializer.save()
UPD!
views.py
class VendorsCreateView(APIView):
"""Create new vendor instances from form"""
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsSerializer
def post(self, request, *args, **kwargs):
data = request.data
if data['nda'] == '':
data['nda'] = None
for contact in data['contacts']:
if contact['email']:
print(contact['email'])
contact['email'] = contact['email'].lower()
serializer = VendorsSerializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
To answer your question: just override save() method for your models as written in docs. There you can assign any values to your model instance directly before saving it in database.
Also, you should probably use models.EmailField for your email fields which will get rid of your lower() check.

Return mean of values in models instead of every value

I am trying to make a Movie project in django. I would like to have one view for film's ratings that can take single rate_value but by GET returns mean of rating values for a film.
I have no idea where should I start. I tried some changes in view and serializer but didnt work. Here are:
views.py:
class RateListView(generics.ListCreateAPIView):
permission_classes = [AllowAny]
serializer_class = RateSerializer
lookup_url_kwarg = 'rateid'
def get_queryset(self):
rateid = self.kwargs.get(self.lookup_url_kwarg)
queryset = Rate.objects.filter(film = rateid)
# .aggregate(Avg('rate_value'))
if queryset.exists():
return queryset
else:
raise Http404
def post(self, request, *args, **kwargs):
serializer = RateSerializer(data = request.data)
if serializer.is_valid():
if Film.objects.filter(pk = self.kwargs.get(self.lookup_url_kwarg)).exists():
serializer.save(film_id = self.kwargs.get(self.lookup_url_kwarg))
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
raise Http404
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.py
class RateSerializer(serializers.ModelSerializer):
class Meta:
model = Rate
fields = ('rate_value',)
and model.py
class Rate(models.Model):
film = models.ForeignKey(Film, on_delete = models.CASCADE)
rate_value = models.IntegerField(validators = [
MinValueValidator(1),
MaxValueValidator(10)
]
)
def __str__(self):
return str(self.film.title) + str(self.rate_value)
Right now it returns single values correctly.
Try something like this:
serializers.py
class RateListSerializer(serializers.Serializer):
avg_rate_value = serializers.FloatField()
class RateSerializer(serializers.ModelSerializer):
class Meta:
model = Rate
fields = ("rate_value", "film")
views.py
class RateListView(generics.ListCreateAPIView):
permission_classes = [AllowAny]
def get_queryset(self):
if self.action == "list":
queryset = Film.objects.all()
film_id = self.request.query_params.get("film_id", None)
if film_id is not None:
queryset = queryset.filter(id=film_id)
return queryset.annotate(avg_rate_value=Avg("rate__rate_value"))
return Rate.objects.all()
def get_seializer_class(self):
if self.action == "list":
return RateListSerializer
return RateSerializer
class Film(models.Model:
...
#property
def mean_rating(self):
ratings = self.ratings.all().values_list('rate_value', flat=True)
if ratings:
return sum(ratings)/len(ratings)
# return a separate default value here if no ratings
class Rate(models.Model):
film = models.ForeignKey(Film, on_delete = models.CASCADE, related_name='ratings')
rate_value = models.IntegerField(validators = [
MinValueValidator(1),
MaxValueValidator(10)
]
)
...
class FilmSerializer(serializers.ModelSerializer):
class Meta:
model = Film
fields = ('id','name','mean_rating',)
Without getting into the architecture of your app, here's a pattern that can be used. The Film object has a computed property that calls ratings, the related_name supplied to a film rating, to get all related ratings and returns the mean.
Properties of models can be used as serializer fields as long as then provide serializable values.
For more on related_name for models in Django, see here - What is `related_name` used for in Django?

Django-rest serializer returning code instead of list of objects

I have an endpoint in my Django-rest application in which I expect to receive the following get response:
{
"my_objects": [
{
"my_object_order": 1,
"related_topics": [{"title": "my_title", "subtitle": "my_subtitle"}, {"title": "my_title2", "subtitle": "my_subtitle2"}],
"collected_at": "2016-05-02T20:52:38.989Z",
}]
}
In order to achieve that, below you can observe my serializers.py
class TopicSerializer(serializers.ModelSerializer):
class Meta:
model = MyTopic
fields = ["title", "subtitle"]
class MyObjectSerializer(serializers.ModelSerializer):
related_topics = TopicSerializer(many=True)
class Meta:
model = MyObject
fields = ("my_object_order",
"related_topics")
def create(self, validated_data):
"""
Saving serialized data
"""
related_topics_list = validated_data.pop("related_topics", [])
obj = MyObject.objects.create(**validated_data)
for topics_data in related_topics_list:
MyTopic.objects.create(trend=trend, **topics_data)
return obj
As suggested, here you can see my models.py
class MyObject(models.Model):
my_object_order = models.IntegerField()
collected_at = models.DateTimeField(auto_now=True)
def __unicode__(self):
return self.story_title
class MyTopic(models.Model):
my_obj = models.ForeignKey(MyObject, related_name="related_topics")
title = models.CharField(max_length=50, blank=False, null=True)
subtitle = models.CharField(max_length=50, blank=True, null=True)
def __unicode__(self):
return self.title
Below you have the excerpt from my views.py
def get(self, request):
params = request.QUERY_PARAMS
# Filtering data
obj_list = my_fun(MyObject, params)
response = {"my_objects": obj_list.values("my_object_order",
"collected_at",
"related_topics")}
return Response(response)
I have looked on the documentation, however I am confused/not understanding fundamentally what I should do.
Your problem is in views.py, you are not using actually the serializer at all. You are just filter some data and return whatever values you get from database (hence the ids only).
I suggest you to check Generic Class Based Views
from myapp.models import MyObject
from myapp.serializers import MyObjectSerializer
from rest_framework import generics
class MyObjectListAPIView(generics.ListAPIView):
queryset = MyObject.objects.all()
serializer_class = MyObjectSerializer
Also if you need any filtering check documentation here. Basically you can filter by fields from model with this snippet
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('field1', 'field2')
PS: You can do the view as normal function, but you have to handle yourself filtering/serialization part, the code may not look as cleaner as you get with class based views.

Categories

Resources