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)
Related
I am trying to acsess fields of a model dynamically(on the basis of call from frontend) in the serializer but unable to do so
code:
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
print("self", self)
fields = self.context['request'].query_params.get('fields')
if fields:
fields = fields.split(',')
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
class ProductTestSerializer(DynamicFieldsModelSerializer, serializers.ModelSerializer):
class Meta:
model = Product
fields = ("id",)
class ProductTestAPIView(generics.ListAPIView):
def get(self, request):
obj = Product.objects.all()
data = ProductTestSerializer(obj, many=True)
s_data = data.data
return Response(s_data)
URL:
http://127.0.0.1:8000/products-test/?fields=id,short_code
It returns the following error:
KeyError: 'request'
at
fields = self.context['request'].query_params.get('fields')
Pass the request when calling the DynamicFieldsModelSerializer, like this:
serializer = DynamicFieldsModelSerializer(request.data, context={'request': request})
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
I have the following two serializers:
class ProgramSerializer(serializers.ModelSerializer):
class Meta:
from radio.models import Program
model = Program
fields = ('id', 'title')
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program_data = ProgramSerializer(read_only=True)
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program', 'program_data',)
They are based on the following models:
class Program(models.Model):
title = models.CharField(max_length=64)
class UserRecentlyPlayed(models.Model):
user = models.ForeignKey(User)
program = models.ForeignKey(Program)
What I'm trying to do is the following: On create, I want to be able create a new instance of UserRecentlyPlayed in the following manner:
{
"user": "...user id ....",
"program": "....program id...."
}
However, when I return a list, I would like to return the following:
[{
"id": "... id .... ",
"user": ".... user id .....",
"program": {"id": "...program id...", "title": "...title..." }
}]
These are called in the following view:
class RecentlyPlayed(generics.ListCreateAPIView):
serializer_class = UserRecentlyPlayedSerializer
This, unfortunately is not working. What is the correct magic for this?
You can rename your program_data in your serializer to program or you can specify source for your nested serializer.
That should return the output of list as you'd like.
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program = ProgramSerializer(read_only=True)
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program',)
or
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program_data = ProgramSerializer(read_only=True, source='program')
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program_data',)
And to support same json input for create, the easiest way is create another serializer for input:
class UserRecentlyPlayedSerializerInput(serializers.ModelSerializer):
program = serializers.PrimaryKeyRelatedField(queryset=Program.objects.all())
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program',)
And use it in your view when request is POST/PUT/PATCH:
class RecentlyPlayed(generics.ListCreateAPIView):
serializer_class = UserRecentlyPlayedSerializer
def get_serializer_class(self):
if self.request.method.lower() == 'get':
return self.serializer_class
return UserRecentlyPlayedSerializerInput
While this works great for a "get", I would like to see that same
result after a create. I still see {"program": "...id...."
For this, you have to change slightly the implementation of create method in your view
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
headers = self.get_success_headers(serializer.data)
oser = UserRecentlyPlayedSerializer(instance)
return Response(oser.data, status=status.HTTP_201_CREATED,
headers=headers)
Firstly create a property named program_data in your model
class Program(models.Model):
title = models.CharField(max_length=64)
class UserRecentlyPlayed(models.Model):
user = models.ForeignKey(User)
program = models.ForeignKey(Program)
#property
def program_data(self):
return self.program
Then in your serializer you do not need to change anything following, it will remain same as below
class ProgramSerializer(serializers.ModelSerializer):
class Meta:
from radio.models import Program
model = Program
fields = ('id', 'title')
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program_data = ProgramSerializer(read_only=True)
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program', 'program_data',)
Ok, I went in a slightly different direction and it works. Instead of using the ListCreateAPIView, I created my own class using ListModeMixin, CreateModelMixin and GenericAPIView. The magic was in overriding the def list class. I also implemented a "return_serializer_class" attribute. That's what did it.
class RecentlyPlayed(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
serializer_class = UserRecentlyPlayedSerializer
return_serializer_class = ProgramSerializer
parser_classes = (JSONParser, MultiPartParser)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.create(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
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.return_serializer_class(queryset, many=True)
return Response({'recently_played': serializer.data})
I'm trying to save a through model which has the following attributes via Django-rest-framework
when sending a POST (I'm trying to create a new instance), I get the following error:
AttributeError at /api/organisation/provider/
'EnabledExternalProvider' object has no attribute 'create'
any ideas as to what i'm doing incorrectly?
the through model in question is:
class EnabledExternalProvider(models.Model):
provider = models.ForeignKey(ExternalProvider, related_name='associatedProvider')
organisation = models.ForeignKey(Organisation, related_name='associatedOrg')
enabled = models.BooleanField(default=True)
tenantIdentifier = models.CharField('Tenant identifer for organisation', max_length = 128, null=True, blank=True)
def __str__(self):
return self.provider + '-' + self.organisation
my view is:
class EnabledExternalProvider(mixins.RetrieveModelMixin, mixins.UpdateModelMixin,generics.GenericAPIView):
serializer_class = ConnectedServiceSerializer
def get_queryset(self):
return EnabledExternalProvider.objects.filter(organisation=self.request.user.organisation_id)
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
# make sure to catch 404's below
obj = queryset.get(organisation=self.request.user.organisation_id)
self.check_object_permissions(self.request, obj)
return obj
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
and my serializer is:
class ConnectedServiceSerializer(serializers.ModelSerializer):
provider=ExternalProviderSerializer(read_only=True)
organisation=OrganisationDetailSerializer(read_only=True)
class Meta:
model = EnabledExternalProvider
fields = ( 'organisation', 'enabled', 'tenantIdentifier')
read_only_fields = ('organisation', 'provider')
I'm POSTing the following:
{"provider":"1","tenantIdentifier":"9f0e40fe-3d6d-4172-9015-4298684a9ad2","enabled":true}
Your view doesn't have that method because you haven't defined it, or inherited from a class that has it; your mixins provide retrieve and update, but not create.
You could add mixins.CreateModelMixin to the inheritance, but at this point you should really be using a ViewSet instead.
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'}