How to serialize mutiple querysets in django rest framework? - python

I have two models that area related to each other. One represents a project and the other one represents the field/area that the project bnelongs to.
I have a view that looks like this:
class ProjetosArea(generics.ListAPIView):
lookup_field = 'id'
serializer_class = ProjetosPreviaSerializer
pagination_class = ProjectPagination
def get_queryset(self):
area_id = self.kwargs['id']
area = AreaConhecimento.objects.get(id=area_id)
projetos = Projeto.objects.filter(area_conhecimento=area_id)
queryset = [area, projetos]
return queryset
I want to write a serializer for this view that looks like this:
{
"area": {---fields of the area---},
"projects": [---The projects that belongs to that area---]
}
How would write a serializer for this? The queryset is a list with the area and the projects belonging to that area

This get_queryset doesn't look right. The idea is to return a single queryset there to perform permissions check etc in there. You should not return two resources like that.
Although if you need it - the code is just a tool. Do with it whatever you need.
I'd suggest to keep get_queryset as it is and overwrite list method instead as that's what you really want to do. Because you are returning two resources, it needs to know how to handle them.
From example I deduct that area_conhecimento is area pk. So something like that should work.
class ProjetosArea(generics.ListAPIView):
lookup_field = 'area_conhecimento'
serializer_class = ProjetosAreaSerializer
pagination_class = ProjectPagination
def list(self, request, *args, **kwargs):
qs = self.filter_queryset(self.get_queryset())
area = qs.first().area_conhecimento
# you will have to adjust it for pagination
serializer = self.get_serializer({
'projects': qs,
'area': area
})
return Response(serializer.data)
And just the serializers are as simple as it gets.
from rest_framework import serializers
class AreaConhecimentoSerializer(serializers.ModelSerializer):
class Meta:
model = AreaConhecimento
fields = '__all__'
class ProjetoSerializer(serializers.ModelSerializer):
class Meta:
model = Projeto
fields = '__all__'
class ProjetosAreaSerializer(serializers.Serializer):
area = AreaConhecimentoSerializer(read_only=True)
projects = ProjetoSerializer(many=True, read_only=True)
But your real problem is that you actually want to get area and its projects. So just do it. You said it yourself The projects that belongs to that area.
class AreaRetrieveAPIView(RetrieveAPIView):
serializer_class = AreaSerializer
pagination_class = AreaPagination
class ProjetoSerializer(serializers.ModelSerializer):
class Meta:
model = Projeto
fields = '__all__'
class AreaSerializer(serializers.ModelSerializer):
projeto = ProjetoSerializer(many=True, read_only=True)
class Meta:
model = AreaConhecimento
fields = '__all__'
and it will return
{
"area": {
"projects": [---The projects that belongs to that area---],
---fields of the area---
},
}

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

Python - Django - modify fields in serializer

i have the following setup:
I have a basic blog and article relation, where i get all blogs and its associated articles:
class BlogSerializer(serializers.ModelSerializer):
articles = ArticleSerializer(many=True, read_only=True)
class Meta:
model = Blog
fields = ('id', 'name', 'articles')
depth = 0
class BlogViewSet(ViewSetMixin, GenericAPIView):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
Now i want to keep things as the are, BUT:
When the list view is called (e.g. api/blogs), only the ids of the articles should be shipped, so i extended my viewset to:
class BlogViewSet(ViewSetMixin, GenericAPIView, ..):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
def get_serializer(self, *args, **kwargs):
# pseudo code
if self.context['request'].action == 'list':
serializer = super(BlogViewSet, self).get_serializer(*args, *kwargs)
serializer.fields['articles'] = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
serializer.is_valid()
return serializer
i just wanted to override the corresponding articles field with a PrimaryKeyRelatedField, so only id´s get shipped.
But i get empty results(no blogs and articles at all) and i have no idea why... any ideas or suggestions?
thanks and greetings!

DRF: Pass serializer depth with a GET parameter

It's possible to change a serializer depth with a GET parameter? For example calling http://localhost:8000/api-auth/?depth=1
The proper way to pass some extra data from view to serializer is the serializer context.
In DRF class-based view, you can (and, actually, should for such purposes) override get_serializer_context() method. In the overridden method you just add to the context, which is just a dictionary, whatever you want.
Simple example, how to do this in view:
class YourView(generics.RetrieveAPIView):
def get_serializer_context(self):
context = super().get_serializer_context()
context['depth'] = self.request.query_params.get('depth', 1)
return context
And the to access it in serializer:
class YourSerializer(serializers.ModelSerializer):
the_depth_from_get_param = serializers.SerializerMethodField()
class Meta:
model = YourModel
fields = [
'the_depth_from_get_param'
]
def get_the_depth_from_get_param(self, obj):
return self.context['depth']
I've solved it in a really simple way, assuming that views are made with class based views or viewsets:
The serializer_class propriety it's actually the serializer, it's a class, so you can access to the depth value with serializer_class.Meta.depth, and assign it a value from the self.request.GET array
Example serializer:
class ItemSerializer(ModelSerializer):
class Meta:
model = Item
fields = '__all__'
Example view:
class ItemList(generics.ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = serializers.ItemSerializer
def get_serializer_class(self):
if(self.request.GET.get('depth')):
if (10 >= int(self.request.GET.get('depth')) >= 0):
self.serializer_class.Meta.depth = int(
self.request.GET.get('depth'))
return self.serializer_class

Django DRF - ListCreateAPIView POST Failing with depth=2

I have a ListCreateAPIViewfor showing a list of contacts, as well as for creating new contacts which uses this serializer:
class ContactPostSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
For POSTing new records, I have to specifically exclude id so that DRF doesn't complain about a null id. But, for listing records with this serializer, the serializer doesn't return the objects in ForeignKey fields. To get these objects, I add depth = 2. So now the serializer looks like this:
class ContactPostSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
depth = 2
However, now, with depth = 2, I can't do POSTs anymore. It complains again of null id values.
Edit: I should add that the errors that come up with I have depth=2 are specific to the models of the Foreign Key objects, not the new record I'm creating.
What am I missing here?
I discovered the problem is that when the serializer has depth=2 that part is not writeable. That's why it was failing. The other thing is that I didn't want to change my URL so that I only had /contacts/ for both listing and for creating. To do that, I had to adjust my class for handling the responses.
Here's what I came up with:
api.py
class ContactViewSet(viewsets.ModelViewSet):
queryset = Contact.objects.all()
serializer_class = ContactSerializer
def create(self, request, *args, **kwargs):
# If we're creating (POST) then we switch serializers to the one that doesn't include depth = 2
serializer = ContactCreateSerializer(data = request.data)
if serializer.is_valid():
self.object = serializer.save()
headers = self.get_success_headers(serializer.data)
# Here we serialize the object with the proper depth = 2
new_c = ContactSerializer(self.object)
return Response(new_c.data, status = status.HTTP_201_CREATED, headers = headers)
return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST)
serializers
class ContactCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ()
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ()
depth = 2
Credit to this SO answer which helped me figure it out: https://stackoverflow.com/a/26741062/717682
Let's say that the link that calls your view is /example/.
If you want to POST data then you can call this like that: "/example/",
If you want to GET data (with depth) you can call this like that: "/example/?depth="yes"
You must have two serializers. One with the depth and one without it.
class ContactPOSTSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
class ContactGETSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
exclude = ('id',)
depth = 2
So then your view will be something like that:
class ExampleView(viewsets.ModelViewSet):
serializer_class = ContactPOSTSerializer
def list(self, request, *args, **kwargs):
depth = self.request.query_params.get('depth', "")
if (depth != "" and depth != "null"):
serializer = ContactGETSerializer(context={'request': request})
return Response(serializer.data)
serializer = ContactPOSTSerializer(context={'request': request})
return Response(serializer.data)
It might not be the best solution but it worked for me :)

django rest framework hide specific fields in list display?

I want to hide specific fields of a model on the list display at persons/ and show all the fields on the detail display persons/jane
I am relatively new to the rest framework and the documentation feels like so hard to grasp.
Here's what I am trying to accomplish.
I have a simple Person model,
# model
class Person(models.Model):
first_name = models.CharField(max_length=30, blank=True)
last_name = models.CharField(max_length=30, blank=True)
nickname = models.CharField(max_length=20)
slug = models.SlugField()
address = models.TextField(max_length=300, blank=True)
and the serializer class
# serializers
class PersonListSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('nickname', 'slug')
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
and the viewsets.
# view sets (api.py)
class PersonListViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonListSerializer
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
at the url persons I want to dispaly list of persons, just with fields nickname and slug and at the url persons/[slug] I want to display all the fields of the model.
my router configurations,
router = routers.DefaultRouter()
router.register(r'persons', api.PersonListViewSet)
router.register(r'persons/{slug}', api.PersonViewSet)
I guess the second configuration is wrong, How can I achieve what I am trying to do?
update:
the output to persons/slug is {"detail":"Not found."} but it works for person/pk
Thank you
For anyone else stumbling across this, I found overriding get_serializer_class on the viewset and defining a serializer per action was the DRY-est option (keeping a single viewset but allowing for dynamic serializer choice):
class MyViewset(viewsets.ModelViewSet):
serializer_class = serializers.ListSerializer
permission_classes = [permissions.IsAdminUser]
renderer_classes = (renderers.AdminRenderer,)
queryset = models.MyModel.objects.all().order_by('-updated')
def __init__(self, *args, **kwargs):
super(MyViewset, self).__init__(*args, **kwargs)
self.serializer_action_classes = {
'list':serializers.AdminListSerializer,
'create':serializers.AdminCreateSerializer,
'retrieve':serializers.AdminRetrieveSerializer,
'update':serializers.AdminUpdateSerializer,
'partial_update':serializers.AdminUpdateSerializer,
'destroy':serializers.AdminRetrieveSerializer,
}
def get_serializer_class(self, *args, **kwargs):
"""Instantiate the list of serializers per action from class attribute (must be defined)."""
kwargs['partial'] = True
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super(MyViewset, self).get_serializer_class()
Hope this helps someone else.
You can override the 'get_fields' method your serializer class and to add something like that:
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
request = self.context.get('request')
if request is not None and not request.parser_context.get('kwargs'):
fields.pop('your_field', None)
return fields
In this case when you get detail-view there is 'kwargs': {'pk': 404} and when you get list-view there is 'kwargs': {}
I wrote an extension called drf-action-serializer (pypi) that adds a serializer called ModelActionSerializer that allows you to define fields/exclude/extra_kwargs on a per-action basis (while still having the normal fields/exclude/extra_kwargs to fall back on).
The implementation is nice because you don't have to override your ViewSet get_serializer method because you're only using a single serializer. The relevant change is that in the get_fields and get_extra_kwargs methods of the serializer, it inspects the view action and if that action is present in the Meta.action_fields dictionary, then it uses that configuration rather than the Meta.fields property.
In your example, you would do this:
from action_serializer import ModelActionSerializer
class PersonSerializer(ModelActionSerializer):
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')
action_fields = {
'list': {'fields': ('nickname', 'slug')}
}
Your ViewSet would look something like:
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
And your router would look normal, too:
router = routers.DefaultRouter()
router.register(r'persons', api.PersonViewSet)
Implementation
If you're curious how I implemented this:
I added a helper method called get_action_config which gets the current view action and returns that entry in the action_fields dict:
def get_action_config(self):
"""
Return the configuration in the `Meta.action_fields` dictionary for this
view's action.
"""
view = getattr(self, 'context', {}).get('view', None)
action = getattr(view, 'action', None)
action_fields = getattr(self.Meta, 'action_fields', {})
I changed get_field_names of ModelSerializer:
From:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
To:
action_config = self.get_action_config()
if action_config:
fields = action_config.get('fields', None)
exclude = action_config.get('exclude', None)
else:
fields = getattr(self.Meta, 'fields', None)
exclude = getattr(self.Meta, 'exclude', None)
Finally, I changed the get_extra_kwargs method:
From:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
To:
action_config = self.get_action_config()
if action_config:
extra_kwargs = copy.deepcopy(action_config.get('extra_kwargs', {}))
else:
extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {}))
If you want to change what fields are displayed in the List vs Detail view, the only thing you can do is change the Serializer used. There's no field that I know of that lets you specify which fields of the Serializer gets used.
The field selection on you serializers should be working, but I don't know what might be happening exactly. I have two solutions you can try:
1 Try to change the way you declare you serializer object
#If you aren't using Response:
from rest_framework.response import Response
class PersonListViewSet(viewsets.ModelViewSet):
def get(self, request):
queryset = Person.objects.all()
serializer_class = PersonListSerializer(queryset, many=True) #It may change the things
return Response(serializer_class.data)
class PersonViewSet(viewsets.ModelViewSet):
def get(self, request, pk): #specify the method is cool
queryset = Person.objects.all()
serializer_class = PersonSerializer(queryset, many=True) #Here as well
#return Response(serializer_class.data)
2 The second way around would change your serializers
This is not the most normal way, since the field selector should be working but you can try:
class PersonListSerializer(serializers.ModelSerializer):
nickname = serializers.SerializerMethodField() #Will get the attribute my the var name
slug = serializers.SerializerMethodField()
class Meta:
model = Person
def get_nickname(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.nickname
def get_slug(self, person):
#This kind of method should be like get_<fieldYouWantToGet>()
return person.slug
I hope it helps. Try to see the APIview class for building your view too.
Somehow close:
If you just want to skip fields in the serilaizer
class UserSerializer(serializers.ModelSerializer):
user_messages = serializers.SerializerMethodField()
def get_user_messages(self, obj):
if self.context.get('request').user != obj:
# do somthing here check any value from the request:
# skip others msg
return
# continue with your code
return SystemMessageController.objects.filter(user=obj, read=False)
I rewrite ModelViewSet list function to modify serializer_class.Meta.fields attribute, code like this:
class ArticleBaseViewSet(BaseViewSet):
def list(self, request, *args, **kwargs):
exclude = ["content"]
self.serializer_class.Meta.fields = [f.name for f in self.serializer_class.Meta.model._meta.fields if f.name not in exclude]
queryset = self.filter_queryset(self.get_queryset()).filter(is_show=True, is_check=True)
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)
return Response(serializer.data)
class BannerArticleViewSet(ArticleBaseViewSet):
queryset = BannerArticle.objects.filter(is_show=True, is_check=True).all()
serializer_class = BannerArticleSerializer
permission_classes = (permissions.AllowAny,)
But it looks not stable, so i will not use it, just share to figure out the best way
My solution.
class BaseSerializerMixin(_ModelSerializer):
class Meta:
exclude: tuple[str, ...] = ()
exclude_in_list: tuple[str, ...] = ()
model: Type[_models.Model]
def get_action(self) -> Optional[str]:
if 'request' not in self.context:
return None
return self.context['request'].parser_context['view'].action
def get_fields(self):
fields = super().get_fields()
if self.get_action() == 'list':
[fields.pop(i) for i in list(fields) if i in self.Meta.exclude_in_list]
return fields
I think it should be like this:
router.register(r'persons/?P<slug>/', api.PersonViewSet)
and you should include a line like this:
lookup_field='slug'
in your serializer class. Like this:
class PersonSerializer(serializers.ModelSerializer):
lookup_field='slug'
class Meta:
model = Person
fields = ('first_name', 'last_name', 'nickname', 'slug', 'address')

Categories

Resources