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
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!
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
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 :)
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')