Return nested serializer in serializer method field - django

I have a method field called followers. I get the list of followers in a SerializerMethodField :
followers = serializers.SerializerMethodField()
I want to format the result with a specific serializer called BaseUserSmallSerializer. How should I implement the method get_followers to achieve that ?

Try this;
followers = BaseUserSmallSerializer(source='get_followers', many=True)
OR
You can use serializer inside methodfield;
def get_followers(self, obj):
followers_queryset = #get queryset of followers
return BaseUserSmallSerializer(followers_queryset, many=True).data

If you prefer a more generic solution:
SerializerMethodNestedSerializer which works same as serializers.SerializerMethodField but wraps the result with the passed serializer and returns a dict
class SerializerMethodNestedSerializer(serializers.SerializerMethodField):
"""Returns nested serializer in serializer method field"""
def __init__(self, kls, kls_kwargs=None, **kwargs):
self.kls = kls
self.kls_kwargs = kls_kwargs or {}
super(SerializerMethodNestedSerializer, self).__init__(**kwargs)
def to_representation(self, value):
repr_value = super(SerializerMethodNestedSerializer, self).to_representation(value)
if repr_value is not None:
return self.kls(repr_value, **self.kls_kwargs).data
Usage
class SomeSerializer(serializers.ModelSerializer):
payment_method = SerializerMethodNestedSerializer(kls=PaymentCardSerializer)
def get_payment_method(self, obj):
return PaymentCard.objects.filter(user=obj.user, active=True).first()
class Meta:
model = Profile
fields = ("payment_method",)
class PaymentCardSerializer(serializers.ModelSerializer):
class Meta:
fields = ('date_created', 'provider', 'external_id',)
model = PaymentCard
The expected output of SerializerMethodNestedSerializer(kls=PaymentCardSerializer)
None or {'date_created': '2020-08-31', 'provider': 4, 'external_id': '123'}

Related

Django REST Framework: Does ModelSerializer have an option to change the fields dynamically by GET or (POST, PUT, DELETE)?

Does ModelSerializer have an option to change the fields dynamically by GET or (POST, PUT, DELETE)?
While GET requires complex fields such as nested serializers, these are not required for (POST, PUT, DELETE).
I think the solution is to use separate serializers for GET and (POST, PUT, DELETE).
But in that case, I'd have to create quite a few useless serializers.
Is there any good solution?
class PlaylistSerializer(serializers.ModelSerializer):
user = UserDetailSerializer(read_only=True)
tracks = serializers.SerializerMethodField()
is_owner = serializers.SerializerMethodField()
is_added = serializers.SerializerMethodField()
is_favorited = serializers.BooleanField()
class Meta:
model = Playlist
fields = (
"pk",
"user",
"title",
"views",
"is_public",
"is_wl",
"created_at",
"updated_at",
"tracks",
"is_owner",
"is_added",
"is_favorited",
)
def get_is_owner(self, obj):
return obj.user == self.context["request"].user
def get_tracks(self, obj):
queryset = obj.track_set
if queryset.exists():
tracks = TrackSerializer(queryset, context=self.context, many=True).data
return tracks
else:
return []
def get_is_added(self, obj):
try:
return obj.is_added
except AttributeError:
return False
class PlaylistUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ("title", "is_public")
first you need to create a class and inherit your serializer from this class as below:
from rest_framework import serializers
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""To be used alongside DRF's serializers.ModelSerializer"""
#classmethod
def default_fieldset(cls):
return cls.Meta.fields
def __init__(self, *args, **kwargs):
self.requested_fields = self._extract_fieldset(**kwargs)
# Fields should be popped otherwise next line complains about
unexpected kwarg
kwargs.pop('fields', None)
super().__init__(*args, **kwargs)
self._limit_fields(self.requested_fields)
def _extract_fieldset(self, **kwargs):
requested_fields = kwargs.pop('fields', None)
if requested_fields is not None:
return requested_fields
context = kwargs.pop('context', None)
if context is None:
return None
return context.get('fields')
def _limit_fields(self, allowed_fields=None):
if allowed_fields is None:
to_exclude = set(self.fields.keys()) - set(self.default_fieldset())
else:
to_exclude = set(self.fields.keys()) - set(allowed_fields)
for field_name in to_exclude or []:
self.fields.pop(field_name)
#classmethod
def all_fields_minus(cls, *removed_fields):
return set(cls.Meta.fields) - set(removed_fields)
then your serializer would be something like this:
class PlaylistSerializer(DynamicFieldsModelSerializer):
class Meta:
model = Playlist
fields = ("pk", "user", "title", "views", "is_public",
"is_wl", "created_at", "updated_at", "tracks", "is_owner",
"is_added", "is_favorited",)
#classmethod
def update_serializer(cls):
return ("title", "is_public")
#classmethod
def view_serializer(cls):
return ("title", "is_public", "is_owner", "is_added")
then you will call your serializer as below:
PlaylistSerializer(instance, fields=PlaylistSerializer.update_serializer()).data

Conditional fields serializer Django

I have a view like this:
class UserDetail(generics.RetrieveDestroyAPIView):
permission_classes = [IsAuthenticatedOrReadOnly]
queryset = User.object.all()
serializer_class = UserSerializer
def get_object(self, queryset=None, **kwargs):
item = self.kwargs.get('pk')
return generics.get_object_or_404(User, id=item)
serializer like this:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'first_name', 'last_name', 'city']
and urls like this:
path('<uuid:pk>', UserDetail.as_view(), name='user_detail')
Can I using just one view and one serializer fetch in one case all data (id, fist_name, last_name and city) and in other case just the city by json? Or maybe I have to create for it especially a new view and serializer like this:
class UserCity(generics.RetrieveDestroyAPIView):
permission_classes = [IsAuthenticatedOrReadOnly]
queryset = User.object.all()
serializer_class = UserJustCitySerializer
def get_object(self, queryset=None, **kwargs):
item = self.kwargs.get('pk')
return generics.get_object_or_404(User, id=item)
and
class UserJustCitySerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['city']
You can try SerializerMethodField. Which will return the field if the condition is true inside your get_your_conditional_field method.
class YourSerializer(serializers.ModelSerializer):
your_conditional_field_name = serializers.SerializerMethodField()
class Meta:
model = model_name
def get_your_conditional_field(self, obj):
# do your conditional logic here
# and return appropriate result
return obj

Reducing queries on serialization with SerializerMethodField

I currently have a serializer which uses two SerializerMethodField that access the same nested object, resulting in two db calls:
# models.py
class Onboarding(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
is_retailer_created = models.BooleanField(default=False)
is_complete = models.BooleanField(default=False)
# views.py
class StateView(RetrieveAPIView):
serializer_class = serializers.UserSerialiser
def get_object(self):
return self.request.user
# serializers.py
class UserSerialiser(serializers.ModelSerializer):
is_onboarded = serializers.SerializerMethodField()
is_loyalty_onboarded = serializers.SerializerMethodField()
class Meta:
model = models.User
fields = ('is_onboarded', 'is_loyalty_onboarded')
def get_is_onboarded(self, obj):
onboarding = obj.onboarding_set.first()
if onboarding:
return onboarding.is_retailer_created
return False
def get_is_loyalty_onboarded(self, obj):
onboarding = obj.onboarding_set.first()
if onboarding:
return onboarding.is_complete
return False
I'd like to roll this into one call if possible. Normally it would be possible to just use prefetch_related, but since the get_object is returning the specific user (and not a queryset) I don't think that solution works here.
Is there a way to prefetch the Onboarding model with the user? Or failing that have a single call to Onboarding instead of two?
Turns out it was a little easier than anticipated. Just needed to save the Onboarding object in __init__.
# serializers.py
class UserSerialiser(serializers.ModelSerializer):
is_onboarded = serializers.SerializerMethodField()
is_loyalty_onboarded = serializers.SerializerMethodField()
class Meta:
model = models.User
fields = ('is_onboarded', 'is_loyalty_onboarded')
def __init__(self, *args, **kwargs):
super(UserSerialiser, self).__init__(*args, **kwargs)
self.onboarding = self.instance.onboarding_set.first()
def get_is_onboarded(self, obj):
if self.onboarding:
return self.onboarding.is_retailer_created
return False
def get_is_loyalty_onboarded(self, obj):
if self.onboarding:
return self.onboarding.is_complete
return False

How to return specific field queryset DRF

I want to create a custom queryset class that returns different fields to pre-define two cases.
when DateField is greater than today
when it's less than today.
In case it's greater return all fields, else return only date_to_open and post_name fields.
views.py
class GroupDetail(generics.RetrieveAPIView):
serializer_class = serializers.GroupDetailsSerializer
permission_classes = (IsAuthenticated, )
def greater(self):
return models.Group.objects.filter(shared_to=self.request.user,
date_to_open__gt=timezone.now()).exists()
def get_queryset(self, *args, **kwargs):
if self.greater():
query_set = models.Group.objects.filter(shared_to=self.request.user,
date_to_open__gt=timezone.now())
else:
query_set = SPECIFIC FIELDS
return query_set
serializers.py
class GroupDetailsSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.name')
images = GroupImageSerializer(many=True, read_only=True)
shared_to = serializers.SlugRelatedField(queryset=models.UserProfile.objects.all(),
slug_field='name', many=True)
class Meta:
model = models.Group
fields = ('id', 'group_name', 'owner', 'group_text', 'created_on', 'date_to_open', 'shared_to',
'images', )
Ok. Thanks to #ArakkalAbu comment I've just overridden get_serializer_class()
views.py
class GroupDetail(generics.RetrieveAPIView):
queryset = models.Group.objects.all()
serializer_class = serializers.GroupDetailsSerializer
permission_classes = (IsAuthenticated, )
def greater(self):
return models.Group.objects.filter(shared_to=self.request.user, date_to_open__gt=timezone.now()).exists()
def get_serializer_class(self):
if self.greater():
return serializers.GroupDetailsSerializer
else:
return serializers.ClosedGroupDetailsSerializer
You can keep using the same logic and use values_list to return specific values out of the Query set. The returned values is also a query set
def get_queryset(self, *args, **kwargs):
if self.greater():
return models.Group.objects.filter(shared_to=self.request.user, date_to_open__gt=timezone.now())
else:
return models.Group.objects.filter(shared_to=self.request.user, date_to_open__lt=timezone.now()).values_list('date_to_open', 'post_name' , flat = True)

Django Rest Framework: using DynamicFieldsModelSerializer for excluding serializer fields

I have a serializer that is used in a couple of endpoints (generics.ListAPIView), but in one of them I need to hide a serializer field. I would prefer to not write a new serializer just to cover this case.
Starting from DRF 3.0 we have dynamic fields for serializers (https://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields), but I'm having some troubles to fully understand how to use them.
I created this serializer:
class TestSerializer(DynamicFieldsModelSerializer):
user_req = UserSerializer(read_only=True)
user_tar = UserSerializer(read_only=True)
class Meta:
model = TestAssoc
fields = ("user_req", "user_tar")
and this is my endpoint:
class TestEndpointListAPIView(generics.ListAPIView):
serializer_class = TestSerializer
permission_classes = [IsAuthenticated]
lookup_field = 'test_username'
def get_queryset(self):
return ...
Now I need to hide the 'user_tar' field from the output, and according to the documentation I should instantiate the serializer with something like:
TestSerializer(fields=('user_req'))
but how should I do this inside my TestEndpointListAPIView? Should I override get_serializer?
Thanks for the help
EDIT:
I've found the following solution, by overriding the get_serialized function:
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
kwargs['fields'] = ['user_req']
return serializer_class(*args, **kwargs)
I'd like to know if it is a good solution. Thanks!
Add this piece of code to __init__ method of the serializer class as suggested in the DRF docs:
class TestSerializer(serializers.ModelSerializer):
user_req = UserSerializer(read_only=True)
user_tar = UserSerializer(read_only=True)
class Meta:
model = TestAssoc
fields = ("user_req", "user_tar")
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(TestSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
And so when you call the serializer from views.py do this :
TestSerializer(queryset, fields=('user_req'))
Alternatively what you can do is define a class
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
Now import this class if you have defined it in some other file and then inherit it using
class TestSerializer(DynamicFieldsModelSerializer):
This way:
class TestSerializer(DynamicFieldsModelSerializer):
user_req = UserSerializer(read_only=True)
user_tar = UserSerializer(read_only=True)
class Meta:
model = TestAssoc
fields = ("user_req", "user_tar")
Now you can do
TestSerializer(queryset, fields=('user_req'))
Update
In the views. Take an example of ListAPIView
class DemoView(ListAPIView):
queryset = TestAssoc.objects.all()
def get(self, request):
try:
queryset = self.get_queryset()
data = TestSerializer(queryset, fields=('user_req')).data
return Response( {"data" : data } ,status=status.HTTP_200_OK)
except Exception as error:
return Response( { "error" : str(error) } , status=status.HTTP_500_INTERNAL_SERVER_ERROR)