Djangorestframework, how can I use a serializer with custom fields that I want passed for the creation method? - django-rest-framework

Let's say I have the following:
class EntityManager(Manager):
def create(label, entity_type, **kwargs):
... do stuff with label and entity type
obj = super().create(**cleanedupkwargs)
obj.addstuffwithlabel(label)
return obj
class Entity(Model):
somefields...
objects = EntityManager()
There's no problem with this and I can call Entity.objects.create(label='foo', entity_type=my_entity_type, other_params=foo)
the issue is I'm now using a serializer and I tried this
class EntityBareboneSerializer(serializers.ModelSerializer):
label = serializers.SerializerMethodField()
entity_type = serializers.SerializerMethodField()
class Meta:
model = Entity
fields = [
'id',
'label',
'entity_type',
]
def validate_label(self, label):
return label
def validate_entity_type(self, entity_type):
return entity_type
def create(self, validated_data):
# do stuff with label and entity type
return Entity.objects.create(**validated_data)
The issue is when is_valid is called the validated_data param comes back empty.
Any idea if it's possible to effectively use my custom create method in the serializer?

You can pre-process the validated data, before creating an instance
def create(self, validated_data):
label = validated_data.pop("label", "some_default_value")
entity_type = validated_data.pop("entity_type", "some_default_value")
obj = Entity.objects.create(**validated_data)
obj.addstuffwithlabel(label)
return obj

Related

AttributeError: 'collections.OrderedDict' object has no attribute 'model_id' and 'model_id' is missing from visible fields

Something strange happened: I was defining an endpoint and initially two fields were visible in the API form: model_id and payload, as given in the model definition:
### models.py:
class CarModel(models.Model):
model_id = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=40)
active = models.BooleanField(default=True)
def __str__(self):
return self.model_id
class Calculator(models.Model):
model = models.ForeignKey(CarModel, on_delete=models.CASCADE)
payload = models.TextField()
def model_id(self):
return self.model.model_id
def __str__(self):
return f"Calculations for {self.model.name}"
### serializers.py:
class CalculatorSerializer(serializers.ModelSerializer):
model_id = serializers.SerializerMethodField()
class Meta:
model = Calculator
fields = ['model_id', 'payload']
def get_model_id(self, obj):
return obj.model_id()
### views.py:
class CalculatorViewSet(viewsets.ModelViewSet):
serializer_class = CalculatorSerializer
queryset = Calculator.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(f"{serializer.data.upper()}", status=status.HTTP_200_OK)
So, both fields were visible, but POST requests ended in the AttributeError: 'collections.OrderedDict' object has no attribute 'model_id'. Trying to fix that, I eventually and accidentally removed model_id from view - it doesn't display in DRF's forms. And the AttributeError still persists.
What is wrong with this piece of code?
OK, it turns out that defining fields in this manner:
fields = '__all__'
makes also the model_id visible. Still, no idea why explicit insert doesn't work.
In case of the other issue, the AttributeError, I had to pull the value out of an OrderedDict. Modified method looks like this:
def get_model_id(self, obj):
return obj["model"].model_id
Beside that, I found one more error inside views.py's create method: serializer.data won't implement upper() method; some key, in my case serializer.data['payload'], has to be referenced, so for example:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
res = {
"payload": f"{serializer.data['payload'].upper()}"
}
return Response(res, status=status.HTTP_200_OK)

How do I save an object using serializer from another serializer in django rest?

I have two models, a parent and it's items
class Parent(models.Model):
#fields
class ParentItems(models.Model):
parentId = #parentId
#fields
and want to create method POST parent/ with ParentItems included, like
{
"parentField": "value",
"parentItems": [
{
"parentItemsField": "value"
}
]
}
So I create Parent view using ModelViewSet and then the serializer:
class ParentItemSerializer(serializers.ModelSerializer):
#serializer
class ParentSerializer(serializers.ModelSerializer):
parentItems = ParentItemSerializer(many=True, required=True, write_only=True)
def _createParentItems(self, parentItem, parent):
for item in parentItem:
item['parent'] = parent.id
parentItemSerializer = ParentItemSerializer(data=item)
if parentItemSerializer.is_valid(raise_exception=True):
itemSerializer.save()
def create(self, validated_data):
with transaction.atomic():
parentItems = validated_data.pop('parentItems')
createdData = super(ParentSerializer, self).create(validated_data)
# create the parent item
self._createParentItems(parentItems, createdData)
return createdData
I add parentItems field in ParentSerializer, inside the create method I pop the parentItems and pass it into _createParentItems to create the items.
but this doesn't work, the parentItems is already an object. when I call parentItemSerializer.is_valid(raise_exception=True) it errors.
How do I save an object from another serializer in django rest?
should I use another serializer field?
Tried using OrderedDict, it doesn't work, it didn't pass the items to validated_data
First change your model like the following code. Then you can use serializer as I wrote for you.
class Parent(models.Model):
#fields
class ParentItem(models.Model):
parent = models.ForeignKey(Parent, on_delete=models.CASCADE)
# other fields
class ParentItemSerializer(ModelSerializer):
class Meta:
model = ParentItem
fields = ('field1', ...)
def create(self, validated_data):
ParentItem.objects.create(field1=validated_data['field2'],...)
return validated_data
class Parent(ModelSerializer):
parentItems = ParentIremSerializer(many=True)
class Meta:
model=Parent
fields = ('parentItems', 'other fields',...)
def create(self, validated_data):
parent = Parent('field1'=validated_data['field1'])
parent.save()
parentItemsList = validate_data.pop('parentItems')
parentItemSer = ParentItemSerialzer(data=parentItemsList,many=True)
if parentItemSer.is_valid():
parentItemSer.save()
return validated_data

django rest_framework serializer parameter

I am using django rest_framework to provide jsGrid json data.
As the rest_framwork example, I create a object fit the jsGrid format
class jsGridResp(object):
def __init__(self, data, itemsCount):
self.data = data
self.itemsCount = itemsCount
and the class based view, create a get function
class RateListViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""
API endpoint that allows user to be viewed or edited
"""
queryset = RateList.objects.all().order_by('-create_date')
serializer_class = RateListSerializer
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
obj = queryset.get(pk=self.request.POST["id"])
self.check_object_permissions(self.request, obj)
return obj
def get(self, request, format=None):
pageIndex = request.GET.get('pageIndex')
pageSize = request.GET.get('pageSize')
sortField = request.GET.get('sortField', 'id')
sortOrder = request.GET.get('sortOrder', 'asc')
sortOrder = "" if sortOrder == "asc" else "-"
rows = RateList.objects.all().order_by("{}{}".format(sortOrder, sortField))
itemsCount = rows.count()
paginator = Paginator(rows, pageSize)
try:
rows = paginator.page(pageIndex)
except PageNotAnInteger:
rows = paginator.page(1)
except EmptyPage:
rows = paginator.page(paginator.num_pages)
result = jsGridResp(data=rows, itemsCount=itemsCount)
serializer = RateListGetSerializer(result)
json = JSONRenderer().render(serializer.data)
return Response(json)
then I create two serializer to serialize the data
class RateListSerializer(serializers.ModelSerializer):
class Meta:
model = RateList
fields = ('id', 'rate_code', 'hr01', 'hr02', 'hr03', 'hr04', 'hr05', 'hr06',
'hr07', 'hr08', 'hr09', 'hr10', 'hr11', 'hr12', 'hr13', 'hr14',
'hr15', 'note', 'create_date', 'update_date')
read_only_fields = ('create_date', 'update_date')
def update(self, instance, validated_data):
result = instance.update(id=instance.id, **validated_data)
return result
def destroy(self, instace, validated_data):
return "{seccuess: true}"
class RateListGetSerializer(serializers.Serializer):
itemsCount = serializers.IntegerField()
data = RateListSerializer(many=True)
but I have many model need to do like these.
can I use just one serializer to serialize all model.
I want to create a jsGridGetSerializer can pass in a model parameter, so I don't need to create many simple serialzer to do the same thing.
Is this possible?
If I understand your problem correctly, you want a generic serializer which accepts a Model variable as a parameter in its Meta class.
One way to do this is to pass in the model name as a url keyword argument, then catching it in views.py. Then you can override the Meta model via overriding the get_serializer_class:
serializers.py
class GenericSerializer(serializers.ModelSerializer):
class Meta:
model = None
views.py
class GenericViewSet(viewsets.ModelViewSet):
def get_queryset(self):
model = self.kwargs.get('model')
return model.objects.all()
def get_serializer_class(self):
GenericSerializer.Meta.model = self.kwargs.get('model')
return GenericSerializer

How to dynamically remove fields from serializer output

I'm developing an API with Django Rest framework, and I would like to dynamically remove the fields from a serializer. The problem is that I need to remove them depending on the value of another field. How could I do that?
I have a serializer like:
class DynamicSerliazer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
elements = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super(DynamicSerliazer, self).__init__(*args, **kwargs)
if self.fields and is_mobile_platform(self.context.get('request', None)) and "url" in self.fields:
self.fields.pop("url")
As you can see, I'm already removing the field "url" depending whether the request has been done from a mobile platform. But, I would like to remove the "elements" field depending on the "type" value. How should I do that?
Thanks in advance
You can customize the serialization behavior by overriding the to_representation() method in your serializer.
class DynamicSerliazer(serializers.ModelSerializer):
def to_representation(self, obj):
# get the original representation
ret = super(DynamicSerializer, self).to_representation(obj)
# remove 'url' field if mobile request
if is_mobile_platform(self.context.get('request', None)):
ret.pop('url')
# here write the logic to check whether `elements` field is to be removed
# pop 'elements' from 'ret' if condition is True
# return the modified representation
return ret
You can create multiple serializers and choose the proper one in view
class IndexView(APIView):
def get_serializer_class(self):
if self.request.GET['flag']:
return SerializerA
return SerializerB
use inheritance to make serializers DRY.
My problem was somewhat similar to yours and I solved it with inheritance.
class StaticSerializer(serializers.ModelSerializer):
class Meta:
model = StaticModel
fields = (
'first_name', 'last_name', 'password', 'username',
'email'
)
class DynamicSerializer(StaticSerializer):
class Meta:
model = StaticModel
fields = (
'first_name',
)
Solution (ViewSet mixin)
I have solved this problem by writing my own ViewSet mixin. It provides quite easy and DRY way to override serializers depending on request action.
class ActionBasedSerializerClassMixin(viewsets.ModelViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_serializer_class(self):
attr_name = f'{self.action}_serializer_class'
if hasattr(self, attr_name):
serializer_class = getattr(self, attr_name)
self.serializer_class = serializer_class
return super().get_serializer_class()
Usage
To use this mixin inherit from it at your viewset (It must be before ModelViewSet parent).
The default serializer is always used as fallback
To use different serializer on list action just set attribute list_serializer_class at your viewset:
class MyViewSet(ViewSet):
serializer_class = MySerializer
list_serializer_class = MyListSerializer
With this code you will have MyListSerializer when action is 'list' and MySerializer for all other actions.
The same patterns works for all other action types: list, create, retrieve, update, partial_update, destroy.
You just need to append _serializer_class to get desired attribute name.
How serailizers should look like
class MySerializer(serializers.ModelSerializer):
some_reverse_rel = MyOtherSerializer(many=True, read_only=True)
class Meta:
model = MyModel
fields = ['field1', 'field2', 'foo', 'bar', 'some_reverse_rel']
class MyListSerailizer(MySerializer): # Note that we inherit from previous serializer
some_reverse_rel = None # Getting rid of reverse relationship
class Meta(MySerializer.Meta):
fields = ['foo', 'bar', 'field1']

Django REST Framework: passing context to a nested serializer

I've a pair of parent-child models/serializers/viewsets - Tool and ToolInput:
models.py:
class Tool(models.Model):
id = models.CharField(max_length=10000, primary_key=True, default=uuid.uuid4, editable=False)
base_command = jsonfield.JSONField(verbose_name="baseCommand")
class ToolInput(models.Model):
tool = models.ForeignKey(Tool, related_name="inputs", on_delete=models.CASCADE)
id = models.CharField(max_length=10000, primary_key=True)
label = models.CharField(max_length=10000, null=True, blank=True)
description = models.CharField(max_length=10000, null=True, blank=True)
type = jsonfield.JSONField()
serializers.py
class ToolSerializer(WritableNestedModelSerializerMixin,
serializers.HyperlinkedModelSerializer):
id = serializers.CharField()
inputs = ToolInputSerializer(many=True)
baseCommand = serializers.JSONField(source="base_command")
class Meta:
model = Tool
fields = ('id', 'inputs', 'baseCommand')
class ToolInputSerializer(WritableNestedModelSerializerMixin,
serializers.HyperlinkedModelSerializer):
class Meta:
model = ToolInput
fields = ('id', 'label', 'description', 'type')
views.py:
class ToolViewSet(viewsets.ModelViewSet):
queryset = Tool.objects.all()
lookup_field = 'id'
serializer_class = ToolSerializer
class ToolInputViewSet(viewsets.ModelViewSet):
lookup_field = 'id'
serializer_class = ToolInputSerializer
def get_queryset(self):
tool_id = self.kwargs['tool_id']
return ToolInput.objects.filter(tool_id=tool_id)
def get_serializer_context(self):
context = super(ToolInputViewSet, self).get_serializer_context()
context["tool"] = Tool.objects.get(id=self.kwargs['tool_id'])
return context
As you can see, I use ToolInputSerializer both as a standalone serializer for ToolInputViewSet and as a nested serializer within ToolViewSet.
When ToolInputSerializer is used as a nested serializer in ToolViewSet, it somehow automagically receives the value of tool argument and assigns it to ToolInput model's tool field (by the way, I feel that it's a totally wrong behavior from architectural point of view - there's no such field as tool on ToolInputSerializer at all and DRF's filling the respective model's field - it should bail out with a Field Does Not Exist error IMO and at least require a write-only field tool on serializer).
But when I use it as a standalone serializer in ToolInputViewSet, I want to assign the value of ToolInput model's tool field to Tool instance, determined by tool_id url parameter, received by ToolInputViewSet in kwargs.
I'm trying to pass the value of that field with serializer context, overriding ToolInputViewSet.get_serializer_context() method, but it's not working. How to do that properly?
Sidenote: I'm pretty tired of the messy and inconsistent, non-uniform automagic of DRF's context handling that pierces layers of Model-Serializer-Field-View architecture. It really needs to be more explicit and customizable.
As for the context, I still don't know how to make it work.
As for how nested serializers work, this is my bad: as you can see, I inherit all the ViewSets from my custom WritableNestedModelSerializerMixin, where I've overridden create() and update() methods to work with nested data structures, so this is my tinkering.
So, as a workaround, I created a separate StandaldonToolInputSerializer and modified ToolInputViewSet, adding the missing tool field to serializer and automatically patching request.data with the Tool reference:
serializers.py
class StandaloneToolInputSerializer(serializers.HyperlinkedModelSerializer):
tool = serializers.PrimaryKeyRelatedField(
write_only=True,
many=False,
queryset=Tool.objects.all()
)
inputBinding = serializers.JSONField(source="input_binding")
class Meta:
model = ToolInput
fields = ('id', 'tool', 'label', 'description', 'type', 'inputBinding')
views.py
class ToolInputViewSet(viewsets.ModelViewSet):
'''
Describes a Tool input.
'''
lookup_field = 'id'
serializer_class = StandaloneToolInputSerializer
def get_queryset(self):
tool_id = self.kwargs['tool_id']
return ToolInput.objects.filter(tool_id=tool_id)
def create(self, request, *args, **kwargs):
request.data["tool"] = self.kwargs['tool_id']
return super(ToolInputViewSet, self).create(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
request.data["tool"] = self.kwargs['tool_id']
return super(ToolInputViewSet, self).update(request, *args, **kwargs)

Resources