Django REST Framework - Limit for Nested Serializer - python

I have the following models.py:
class Category(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, blank=False)
class Movie(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, blank=False)
category = models.ForeignKey(Category,related_name='movies',
on_delete=models.CASCADE,
blank=True,
null=True)
As you can see, there is a ForeignKey relationship between the two classes. A Category can have multiple movies, but a Movie belongs to only one Category.
My serializers.py looks like the following:
class CategorySerializer(serializers.HyperlinkedModelSerializer):
movies = MoviesSerializer(many=True, read_only=True)
class Meta:
model = Category
fields = ('url','id','created','name', 'movies')
class MovieSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Movie
fields = ('url','id','created','name', 'category')
So, I have nested serializers. When I deserialize the content of the categories, then it shows me also the nested movies in the JSON output as I expected.
But it shows me ALL movies belonging to a particular category in the JSON output. How can I limit this number ?
I tried this solution but it did not worked for me because I use serializers.HyperlinkedModelSerializer. In that provided solution they used serializers.ModelSerializer. I got this error when I tried that solution:
AssertionError: `HyperlinkedIdentityField` requires the request in the serializer context. Add `context={'request': request}` when instantiating the serializer.
UPDATE:
Here is my views.py:
class AllCategories(generics.ListAPIView):
'''
This class class-based view lists
all the categories created
'''
serializer_class = CategorySerializer
queryset = Category.objects.all()
def list(self, request, *args, **kwargs):
'''
standard method that we override just to
put the string 'allCategories' before the dataset
'''
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
# the only part we change
return Response({'allCategories': serializer.data})
class AllMovies(generics.ListAPIView):
'''
This class lists all the movies
'''
# define the serializer class
serializer_class = MovieSerializer
queryset = Movie.objects.all()
def list(self, request, *args, **kwargs):
'''
standard method that we override just to
put the string 'allMoviesOfUser' before the dataset
'''
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
# the only part we change
return Response({'allMoviesOfUser': serializer.data})

Related

Django Rest Framework, how to use serializers.ListField with model and view?

I want to store an array of integers in the day_of_the_week field. for which I am using the following code
models.py
class Schedule(models.Model):
name = models.CharField(max_length=100)
day_of_the_week = models.CharField(max_length=100)
serializers.py
class ScheduleSerializer(serializers.ModelSerializer):
day_of_the_week = serializers.ListField()
class Meta():
model = Schedule
fields = "__all__"
Views.py
# schedule list
class ScheduleList(APIView):
def get(self, request):
scheduleData = Schedule.objects.all()
serializer = ScheduleSerializer(scheduleData, many=True)
return Response(serializer.data)
def post(self, request):
serializer = ScheduleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("Schedule Added")
Data save successfully but when I try to get data it returns data in this format
"day_of_the_week": [
"[2",
" 1]"
],
is there any way to get an array of integers as a response?
While saving try to add the child field in the serializer:
class ScheduleSerializer(serializers.ModelSerializer):
day_of_the_week = serializers.SerializerMethodField()
def get_day_of_the_week(self, instance):
return instance.day_of_the_week[1:-1].split(',')
class Meta():
model = Schedule
fields = "__all__"

I want to nest a model into the Serialiazer. Category' object has no attribute 'brand_set'

I want to nest a model into the serializer. Like on list of Category, there should be fields from Brand model, But I m getting error by on setting this way ?
models.py
class Category(models.Model):
title = models.CharField(max_length=50)
timestamp = models.DateTimeField(auto_now_add=True)
#....
class Brand(models.Model):
title = models.CharField(max_length=50)
category = models.ForeignKey(
Category, blank=True, null=True, on_delete=models.SET_NULL, related_name="category")
#....
Serializers
class CategorySerializerNested(serializers.ModelSerializer):
brands = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Category
fields = '__all__'
def get_brands(self, obj):
brands = obj.brand_set.all() #this thin popping error how to fix that....
serializer = BrandSerializerNested(brands, many=True)
return serializer.data
class BrandSerializerNested(serializers.ModelSerializer):
products = serializers.SerializerMethodField(read_only=True)
def get_products(self, obj):
products = obj.product_set.all()
serializer = ProductSerializer(products, many=True)
return serializer.data
class Meta:
model = Brand
fields = '__all__'
View.py
#api_view(['GET'])
def getCategoryWithBrands(request, pk):
category = Category.objects.get(id=pk)
serializer = CategorySerializerNested(category, many=False)
return Response(serializer.data)
url.py
path('nested/<str:pk>/', views.getCategoryWithBrands,
name="category-with-brands"),
Error:
AttributeError: 'Category' object has no attribute 'brand_set'
[02/Feb/2022 03:24:49] "GET /api/v1/categories/nested/1/ HTTP/1.1" 500 125121
I'm sure im doing something illogical but i dont know now, please help me to fix this , if there's any better way to do that please also help there as well. Thanks
Since you specified related_name='category', you obtain the related Brands with:
def get_brands(self, obj):
brands = obj.category.all()
serializer = BrandSerializerNested(brands, many=True)
return serializer.data
But that does not make much sense: the related_name=… [Django-doc] specifies the name of the relation in reverse so obtaining the Brands for a given Category, you thus can rename these to:
class Brand(models.Model):
category = models.ForeignKey(
Category,
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='brands'
)
#…
and work with a subserializer:
def get_brands(self, obj):
brands = obj.brands.all()
serializer = BrandSerializerNested(brands, many=True)
return serializer.data
or just define BrandSerializerNested first and use:
class CategorySerializerNested(serializers.ModelSerializer):
brands = serializers.BrandSerializerNested(read_only=True)
class Meta:
model = Category
fields = '__all__'

How do I dynamically filter fields returned in django rest api get request based on the user making the request?

I have a photoshoot api that allows photographers to post shoot photos from which preview and watermarded versions are derived. Editors and clients can both select which photos will be edited, but the editor should only see the preview without watermark and the clients should only see the watermarked versions.
I was wondering how these different distincitons can be made in the DRF queryset.
My model:
class Unedited(models.Model):
shoot = models.ForeignKey(
Shoot, on_delete=models.CASCADE, null=True, related_name="shoot_photo"
)
original = models.ImageField(null=True, blank=False, upload_to=shoot_upload_to)
preview = models.ImageField(null=True, blank=True, upload_to=shoot_upload_to)
watermarked = models.ImageField(null=True, blank=True, upload_to=shoot_upload_to)
upload_time = models.DateTimeField(auto_now_add=True)
My Serializer:
class UneditedSerializer(serializers.ModelSerializer):
class Meta:
model = Unedited
fields = "__all__"
def create(self, validated_data):
validated_data["preview"] = reduce_resolution(validated_data["original"])
validated_data["watermarked"] = add_watermark(validated_data["preview"])
img_obj = Unedited.objects.create(**validated_data)
img_obj.save()
return img_obj
My view:
class UneditedViewSet(viewsets.ModelViewSet):
if not TESTING:
permission_classes = (PhotosPermission,)
serializer_class = UneditedSerializer
def get_queryset(self):
return Unedited.objects.filter(**self.request.query_params)
I was able to solve this issue by creating separate serializers with different fields for the different cases.
UneditedSerializer remained as is but I created other serializers:
class UneditedMarkedSerializer(serializers.ModelSerializer):
class Meta:
model = Unedited
fields = ("watermarked", "shoot", "upload_time")
class UneditedPreviewSerializer(serializers.ModelSerializer):
class Meta:
model = Unedited
fields = ("preview", "shoot", "upload_time")
I then modified the viewset to check against the user making the request to determine which serializer to use. Like so:
class UneditedViewSet(viewsets.ModelViewSet):
if not TESTING:
permission_classes = (PhotosPermission,)
serializer_class = UneditedSerializer
def get_queryset(self):
return generic_queryset(Unedited, self.request.query_params)
def list(self, request):
queryset = self.get_queryset()
if request.user.is_anonymous or request.user.role == "CLIENT":
serializer = UneditedMarkedSerializer(queryset, many=True)
elif request.user.role == "EDITOR":
print(request.user.role)
serializer = UneditedPreviewSerializer(queryset, many=True)
elif request.user.role in ("ADMIN", "STAFF"):
serializer = UneditedSerializer(queryset, many=True)
return Response(serializer.data)
And now it works as intended.

Filtering querysets for nested models

I'm making a News App API and I want to create an APIView with comments for a speicific Post that also lets users posting comments for the specific post.
These are my models (simplified):
Post:
class Post(models.Model):
title = models.CharField(max_length=250)
text = models.TextField()
Comment:
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.CharField(max_length=200)
text = models.TextField()
And view:
class CommentList(generics.ListCreateAPIView):
queryset = Comment.objects.filter(post=???)
serializer_class = CommentSerializer
EDIT: I would also like my url path to look like this (or similar):
urlpatterns = [
...
path('posts/<int:pk>/comments/', CommentList.as_view())
]
My questions:
How do I create a list of comments for an instance of Post?
Is it the correct approach or should I try something else?
If your url for comments is something like: /posts/post_id/comments/
# serializer
class CommentSerializer(serializers.ModelSerializer):
author = SomeAuthorSerializer(read_only=True)
class Meta:
model = Comment
fields = ('author', 'text')
# view
class CommentViewSet(viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.ListModelMixin):
queryset = Comment.objects
serializer_class = CommentSerializer
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
try:
self.post = Post.objects.get(pk=self.request.query_params.get('post_id'))
# Prefetch post object in this situation let you check permissions
# eg.:
if self.post.author != self.request.user:
raise PermissionDenied()
# remember that permission classes should be used !
except Post.DoesNotExist:
raise NotFound()
# It will filter your comments
def filter_queryset(self, queryset):
queryset = queryset.filter(post=self.post)
return super().filter_queryset(queryset)
# It will automatically bypass post object and author
# to your serializer create method
def perform_create(self, serializer):
serializer.save(post=self.post, author=self.request.user)
This is the solution that worked for me:
class CommentByPostList(generics.ListCreateAPIView):
queryset = Comment.objects.all()
serializer_class = CommentListSerializer
def get_queryset(self):
return Comment.objects.filter(post=self.kwargs['pk'])
Create a PostDetailView to GET a specific post then have the serializer return the comments for that Post.
# serializers.py
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class PostSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True)
class Meta:
model = Post
fields = ('title', 'text', 'comments') # Comments field is recognized by the related_name set in your models.
# views.py
class PostDetailView(generics.RetreiveApiView):
permission_classes = (IsAuthenticated,)
serializer_class = PostSerializer
queryset = Post.objects.all()

Django rest framework serializing many to many field

How do I serialize a many-to-many field into list of something, and return them through rest framework? In my example below, I try to return the post together with a list of tags associated with it.
models.py
class post(models.Model):
tag = models.ManyToManyField(Tag)
text = models.CharField(max_length=100)
serializers.py
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ("text", "tag"??)
views.py
class PostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
You will need a TagSerializer, whose class Meta has model = Tag. After TagSerializer is created, modify the PostSerializer with many=True for a ManyToManyField relation:
class PostSerializer(serializers.ModelSerializer):
tag = TagSerializer(read_only=True, many=True)
class Meta:
model = Post
fields = ('tag', 'text',)
Answer is for DRF 3
This is what I did, let´s suppose a Book can have more than one author and an Author can have more than one book:
On Model:
class Author(models.Model):
name = models.CharField(max_length=100, default="")
last_name = models.IntegerField(default=0)
class Book(models.Model):
authors = models.ManyToManyField(Author, related_name="book_list", blank=True)
name = models.CharField(max_length=100, default="")
published = models.BooleanField(default=True)
On Serializers:
class BookSerializer(serializers.ModelSerializer):
authors = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all(), many=True)
class Meta:
model = Book
fields = ('id', 'name', 'published', 'authors')
class AuthorSerializer(serializers.ModelSerializer):
book_list = BookSerializer(many=True, read_only=True)
class Meta:
model = Author
fields = ('id', 'name', 'last_name', 'book_list')
Adding to #Brian's answer
"tags": [{"name": "tag1"}] can be simplified to "tags": ["tag1", "tag2",...] in this way:
class TagListingField(serializers.RelatedField):
def to_representation(self, value):
return value.name
class PostSerializer(serializers.ModelSerializer):
tag = TagListingField(many=True, read_only=True)
class Meta:
...
More info here: https://www.django-rest-framework.org/api-guide/relations/#custom-relational-fields
The default ModelSerializer uses primary keys for relationships. However, you can easily generate nested representations using the Meta depth attribute:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ("text", "tag")
depth = 1
As mentioned in the documentation :
The depth option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
This works for me.
tag = TagSerializer(source="tag", read_only=True, many=True)
Django 2.0
For many to many field, if you want specific one:
class QuestionSerializer(serializers.ModelSerializer):
topics_list = serializers.SerializerMethodField()
def get_topics_list(self, instance):
names = []
a = instance.topics.get_queryset()
for i in a:
names.append(i.desc)
return names
class Meta:
model = Question
fields = ('topics_list',)
In the serializer on init method you can pass the queryset to the field and rest_framework valide the ids on that queryset
1) first extend your serializer from serializers.ModelSerializer
class YourSerializer(serializers.ModelSerializer):
2) include the field on the meta class
class YourSerializer(serializers.ModelSerializer):
class Meta:
fields = (..., 'your_field',)
3) in the init method:
def __init__(self, *args, **kwargs):
super(YourSerializer, self).__init__(*args, **kwargs)
self.fields['your_field].queryset = <the queryset of your field>
You can limit the queryset for that field under any argument using filter or exclude like normally you do. In case that you want include all just use .objects.all()
models.py
class Tag(models.Model):
name = models.CharField(max_length=100)
# ===============
# ... rest of the fields ...
class Post(models.Model):
tag = models.ManyToManyField(Tag)
text = models.CharField(max_length=100)
serialiazers.py
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
class PostSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ("text", "tag")
views.py
## FUNCTION BASED VIEW
def fbvPost_ListView(request):
# list
if request.method == "GET":
posts = Post.objects.all()
serializer = PostSerializer(instance=posts, many=True)
return JsonResponse(serializer.data, safe=False)
return JsonResponse({"success": False})
# ===========================================================
## CLASS BASED VIEW
class cbvPost_ListView(viewsets.ReadOnlyModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
NB: Tag, Post are two models & we need to serialize them. Here, Post model have a dependency of Tag models, so here we explicitly mention it, [tags = TagSerializer(many=True, read_only=True)] or its return it's primary field value.
DETAILS HERE
Hi I will be showing many to many for update and create. The context is the event can have many dances and dances can have many event.
The request will be as followed.
{
"competition": 2,
"title": "the title",
"dances":[ {"id":1},{"id":2}],
"description": "the desc"
}
The Create Function will be as followed.
def create(self, validated_data):
try:
dance_ids = []
for dance in self.initial_data['dances']:
if 'id' not in dance:
raise serializers.ValidationError({'detail': 'key error'})
dance_ids.append(dance['id'])
new_event = models.Event.objects.create(**validated_data)
if dance_ids:
for dance_id in dance_ids:
new_event.dances.add(dance_id)
new_event.save()
return new_event
except Exception as e:
raise serializers.ValidationError({'detail': e})
The Update Function will be as followed.
def update(self, instance, validated_data):
# Delete all records of genres.
try:
for current_genre in instance.dances.all():
instance.dances.remove(current_genre)
# Repopulate genres into instance.
for dance in self.initial_data['dances']:
if 'id' not in dance:
raise serializers.ValidationError({'detail': 'key error'})
dance_obj = models.Dance.objects.get(pk=dance['id'])
instance.dances.add(dance_obj)
event_updated = super().update(instance, validated_data)
return event_updated
except Exception as e:
raise serializers.ValidationError({'detail': e})
If you want to just do "dances":[1,2] instead, just make some amendments to the
for dance in self.initial_data['dances']:
if 'id' not in dance:
raise serializers.ValidationError({'detail': 'key error'})
dance_ids.append(dance['id'])
part. I hope this will be able to help yall out! :)
First, Tag needs its own serializer too
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
Then in your PostSerializer, add one line
class PostSerializer(serializers.ModelSerializer):
tag = TagSerializer(read_only=True, many=True).data
class Meta:
model = Post
fields = ("text", "tag")
This will make it so your Tag field in Post is an array of tag ids. if you don't put the ".data" part, it will put all of the attributes of tag, which is probably too much in most cases
You can use serializers.SlugRelatedField() or serializers.StringRelatedField(many=True) Serializer relations
In your case :
class PostSerializer(serializers.ModelSerializer):
tag = serializers.StringRelatedField(many=True) # this will return a list
class Meta:
model = Post
fields = ('tag', 'text',)

Categories