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

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

Related

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

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

Custome the display's field in django REST

I use Django REST and I would know if it is possible to customise the display of attributes in the json response.
Exemple :
class MyModel(models.Model):
name = models.CharField(max_length=300)
and my serializer :
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ['name']
But instead to see {'name' : 'its value'}, I would see {'My customed model name' : 'its value'}.
Do you think that it's possible?
Thank you very much.
You can override the to_representation method of the serializer to change the name of the field:
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ['name']
def to_representation(self, instance):
ret = super().to_representation(instance)
# ret is an OrderedDict, so this will change the order of the result.
ret['custom_name'] = ret.pop('name')
return ret
def to_internal_value(self, data):
# if you want to write to the serializer using your custom name.
data['name'] = data.pop('custom_name')
return super().to_internal_value(data)
One way you could do it is using a SerializerMethodField (https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield)
class MyModelSerializer(serializers.ModelSerializer):
my_customed_model_name = serializers.SerializerMethodField()
def get_my_customed_model_name(self, obj):
return obj.name
class Meta:
model = MyModel
Although if you want the field name to have spaces in it, this solution won't work for you.
You can do this in this way
class MyModelSerializer(serializers.ModelSerializer):
other_name = serializers.CharField(source='name')
class Meta:
model = MyModel
fields = ['other_name']

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

Passing argument from view to Custom RelatedField serializer

How can I pass an argument to a serializers.RelatedField class from views.py. I need to pass language_id to query Language.objects model within that RelatedField.
I am not sure if I took a right approach to this issue. What I want to achieve is to present information about genres associated to a movie from database model about depending on the language. The MovieGenre model has genre ID field which I want to replace with actual Genre name.
My serialiser.py
class GenreField(serializers.RelatedField):
def to_representation(self, value, language_id=1):
genre_name = GenresVideo.objects.get(genre_id=value, language_id=language_id)
return genre_name.name
class MovieGenresSerializer(serializers.ModelSerializer):
genre_id = GenreField(read_only=True)
class Meta:
model = MoviesGenres
As you see, here I query Language.objects with default value but I would like to pass it from views (language_id).
My views.py:
class MovieGenresTestViewSet(viewsets.ModelViewSet):
lookup_field = 'movie'
queryset = MoviesGenres.objects.all()
serializer_class = MovieGenresSerializer
def list(self, request, language_pk):
queryset = MoviesGenres.objects.all()
serializer = MovieGenresSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, movie, language_pk):
queryset = MoviesGenres.objects.filter(movie=movie)
serializer = MovieGenresSerializer(queryset, many=True)
return Response(serializer.data)
And my urls.py:
router.register(r'lang', LanguagesViewSet, base_name='lang')
mov_gen = routers.NestedSimpleRouter(router, r'lang', lookup='language')
mov_gen.register(r'mg', MovieGenresTestViewSet, base_name='mg')
url(r'^api/', include(genre_spec.urls))
My models.py
class Languages(models.Model):
name = models.CharField(unique=True, max_length=255)
short_name = models.CharField(unique=True, max_length=4, blank=True, null=True)
active = models.BooleanField(default="")
class Meta:
managed = False
db_table = 'languages'
ordering = ('id',)
class GenresVideo(models.Model):
genre_id = models.IntegerField()
language = models.ForeignKey('Languages')
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'genres_video'
unique_together = (('genre_id', 'language'),)
ordering = ('genre_id',)
class MoviesGenres(models.Model):
movie = models.ForeignKey(Movies)
genre_id = models.IntegerField()
class Meta:
managed = False
db_table = 'movies_genres'
unique_together = (('movie', 'genre_id'),)
Through the urls routes, I can get a correct response from API including the language_id. I just need to pass it to the view somehow.
Thanks a lot for help!
I'll try to answer to your first question, with the easiest implementation possible: SerializerMethodField. Because we will get the language id via the context passed to the serializer, we should either generate the context for the serializer, or let the framework do that for us.
Now to the problem at hand: you aren't filtering the queryset (MoviesGenres) by language per se. Thus, we can avoid overwriting the list and retrieve methods. Nevertheless, the router mechanism will inject in kwargs for the view method the language_pk parameter - that's the parameter that we will retrieve from within the serializer context:
class MovieGenresSerializer(serializers.ModelSerializer):
genre = searializers.SerializerMethodField()
class Meta:
model = MoviesGenres
def get_genre(self, instance):
# get the language id from the view kwargs
language_id = self.context['view'].kwargs['language_pk']
# get the genre
try:
genre_name = GenresVideo.objects.get(genre_id=instance.genre_id, language_id=language_id).name
except GenresVideo.DoesNotExist:
genre_name = None
# return the formatted output
return genre_name
class MovieGenresTestViewSet(viewsets.ModelViewSet):
lookup_field = 'movie'
queryset = MoviesGenres.objects.all()
serializer_class = MovieGenresSerializer

Resources