DRF SerializerMethodField how to pass parameters - python

Is there a way to pass paremeters to a Django Rest Framework's SerializerMethodField?
Assume I have the models:
class Owner(models.Model):
name = models.CharField(max_length=10)
class Item(models.Model):
name = models.CharField(max_length=10)
owner = models.ForeignKey('Owner', related_name='items')
itemType = models.CharField(max_length=5) # either "type1" or "type2"
What I need is to return an Owner JSON object with the fields: name, type1items, type2items.
My current solution is this:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = models.Item
fields = ('name', 'itemType')
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type1")
return ItemSerializer(queryset, many=True).data
def getType2Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type2")
return ItemSerializer(queryset, many=True).data
This works. But it would be much cleaner if I could pass a parameter to the method instead of using two methods with almost the exact code. Ideally it would look like this:
...
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getItems', "type1")
type2items = serializers.SerializerMethodField(method_name='getItems', "type2")
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data
In the docs SerializerMethodField accepts only one parameter which is method_name.
Is there any way to achieve this behaviour using SerializerMethodField? (The example code here is overly simplified so there might be mistakes.)

There is no way to do this with the base field.
You need to write a custom serializer field to support it. Here is an example one, which you'll probably want to modify depending on how you use it.
This version uses the kwargs from the field to pass as args to the function. I'd recommend doing this rather than using *args since you'll get more sensible errors, and flexibility in how you write your function/field definitions.
class MethodField(SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
# use kwargs for our function instead, not the base class
super().__init__(method_name)
self.func_kwargs = kwargs
def to_representation(self, value):
method = getattr(self.parent, self.method_name)
return method(value, **self.func_kwargs)
Using the field in a serializer:
class Simple(Serializer):
field = MethodField("get_val", name="sam")
def get_val(self, obj, name=""):
return "my name is " + name
>>> print(Simple(instance=object()).data)
{'field': 'my name is sam'}

You could just refactor what you have:
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
return getItems(ownerObj,"type1")
def getType2Items(self, ownerObj):
return getItems(ownerObj,"type2")
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data

Related

How to add new serializer field along with the all model fields?

Here I have a model which has so many fields. So I want to use __all__ to return all the fields. But now I needed to add new field image_url so I customize a serializer like this but now with this I need to put all the model fields in the Meta class like this fields=['name','..', 'image_url'] in order to return the image_url.
Is there any way to return image_url without specifying it in the Meta.fields ?
I mean I don't want to write all the model fields in the Meta.fields (since the fields are too many) and want to return the image_url also.
serializers.py
class MySerializer(ModelSerializer):
image_url = serializers.SerializerMethodField('get_image_url')
class Meta:
model = MyModel
fields = '__all__'
def get_image_url(self, obj):
return obj.image.url
You can try to subclass te serializer:
class MySerializer(ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
class MyChildSerializer(MySerializer):
image_url = serializers.SerializerMethodField()
class Meta:
fields = MySerializer.Meta.fields + ['image_url']
def get_image_url(self, obj):
return obj.image.url
Never tried something like this, but since Meta.fields is a list you can perform basic python operations on it.
ps. If you're using pattern get_<field_name> for getter, you do not need to specify it in SerializerMethodField arguments.
Try this:
class MySerializer(ModelSerializer):
image_url = serializers.SerializerMethodField()
class Meta:
model = MyModel
fields = [f.name for f in MyModel._meta.fields] + ['image_url']
def get_image_url(self, obj):
return obj.image.url

Django Rest Framework + django_filter: filter for name or none if nothing is provided

I'm using Django rest Framework together with django_filters.
(I've tried to simplify/reduce the code here as much as possible)
How can I force django_filters to filter for constraints__name=None (or constraints=None) if no constraint is specified in the request?
Let's say I have this model:
class Resource(models.Model):
constraints = models.ManyToManyField(Feature, related_name='constraint+', blank=True)
class Feature(models.Model):
name = models.CharField(max_length=255, blank=False, unique=True)
And this view and filter:
class ResourceFilter(FilterSet):
constraints = django_filters.CharFilter(name='constraints__name')
class Meta:
model = Resource
fields = ['constraints']
class ResourceViewSet(viewsets.ModelViewSet):
serializer_class = ResourceSerializer
filter_class = ResourceFilter
filter_backends = (filters.DjangoFilterBackend,)
/api/resource/?constraints=testconstraint works fine, but I want /api/resource/ to only return the Resources that have no constraints.
I can reduce the queryset, but it feels like something django_filters could solve. Is it?:
def get_queryset(self):
if 'constraints' not in self.request.query_params:
return Resource.objects.filter(constraints=None)
else:
return Resource.objects.all()
You can override the qs property on the FilterSet subclass, where you can manipulate the filtered query and access the request object:
Try with this:
class ResourceFilter(FilterSet):
constraints = django_filters.CharFilter(name='constraints__name')
class Meta:
model = Resource
fields = ['constraints']
#property
def qs(self):
parent_qs = super(ResourceFilter, self).qs
if 'constraints' in self.request.query_params:
return parent_qs
else:
return parent_qs.filter(constraints=None)
Subclass CharFilter overriding filter:
class OrNoneCharFilter(CharFilter):
def filter(self, qs, value):
if value is None:
return qs.filter(constraints=None)
return super().filter(qs, value)

Django Rest Framework How to update SerializerMethodField

I have a serializer like this:
class PersonSerializer(serializers.ModelSerializer):
gender = serializers.SerializerMethodField()
bio = BioSerializer()
class Meta:
model = Person
fields = UserSerializer.Meta.fields + ('gender', 'bio',)
def get_gender(self, obj):
return obj.get_gender_display()
I used this to display "Male" and "Female"(insted of "M" of "F") while performing GET request.
This works fine.
But now I am writing an patch method for the model and SerializerMethodField() has read_only=True. So I am not getting value passed for gender field in serializer.validated_data(). How to overcome this issue?
So if I understand you correctly, you want to send {'gender': 'Male'} in your PATCH request.
Therefor, you have to tell your serializer how to convert your representation i.e. 'Male' into the internal value.
As you can see in source, SerializerMethodField only covers the conversion from internal value to the representation.
You can implement a custom SerializerField that performs the necessary conversions. A naive implementation could something like this:
class GenderSerializerField(serializers.Field):
VALUE_MAP = {
'M': 'Male',
'F': 'Female'
}
def to_representation(self, obj):
return self.VALUE_MAP[obj]
def to_internal_value(self, data):
return {k:v for v,k in self.VALUE_MAP.items()}[data]
class PersonSerializer(serializers.ModelSerializer):
gender = GenderSerializerField()
...
Note that this untested and lacks any validation, check out the DRF docs on custom fields.
Aside from accepted answer, there can be other simpler hooks. If 'create' and 'update' worked as you wanted before modifiying gender field, then you can do as follow to get everything to default for create and update requests.
Do not user SerializerMethodField. Instead override serializer representation.
class PersonSerializer(serializers.ModelSerializer):
bio = BioSerializer()
class Meta:
model = Person
fields = UserSerializer.Meta.fields + ('bio',)
def to_representation(self, obj):
ret = super().to_representation(obj)
ret['gender'] = obj.get_gender_display()
return ret
Override the __init__ method .
.
class PersonSerializer(serializers.ModelSerializer):
bio = BioSerializer()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
if self.context['request'].method in ['GET']:
self.fields['gender'] = serializers.SerializerMethodField()
except KeyError:
pass
class Meta:
model = Person
fields = UserSerializer.Meta.fields + ('bio',)
def get_gender(self, obj):
return obj.get_gender_display()

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

Django Serializer Display Model Field as Dictionary

I apologize, new to Django. I've been scouring the documentation and haven't been able to find the answer to this.
I have a model "Foo" that has a field "bar", which is a dictionary I store as JSON in a TextField. I want a GET request to display this field as a dictionary, but when I make the request, the dictionary is displayed as a single string in JSON format.
To summarize my code:
models:
class Foo(models.Model):
bar = models.TextField(blank=True, default="{}")
def getBar(self):
return json.loads(bar)
Serializers:
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = ("bar")
read_only_fields = ("bar")
def create(self, data):
return Foo.objects.create(**data)
views:
class FooList(generics.ListAPIView):
queryset = []
for foo in Foo.objects.all():
foo.bar = json.loads(foo.bar)
# Printing type of foo.bar here gives "type <dict>"
queryset.append(foo)
serializer_class = FooSerializer
Thanks!
You can add a SerializerMethodField to your ModelSerializer class like below:
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = ('bar',)
read_only_fields = ('bar',) # Not required, because
# SerializerMethodField is read-only already
bar = serializers.SerializerMethodField('get_bar_dict')
def get_bar_dict(self, obj):
return json.loads(obj.bar) # This gets the dict and returns it
# to the SerializerMethodField above
# Below is the rest of your code that I didn't touch
def create(self, data):
return Foo.objects.create(**data)

Categories

Resources