Mixin common fields between serializers in Django Rest Framework - python

I have this:
class GenericCharacterFieldMixin():
attributes = serializers.SerializerMethodField('character_attribute')
skills = serializers.SerializerMethodField('character_skill')
def character_attribute(self, obj):
character_attribute_fields = {}
character_attribute_fields['mental'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.mental_attributes}
character_attribute_fields['physical'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.physical_attributes}
character_attribute_fields['social'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.social_attributes}
return character_attribute_fields
def character_skill(self, obj):
character_skill_fields = {}
character_skill_fields['mental'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.mental_skills}
character_skill_fields['physical'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.physical_skills}
character_skill_fields['social'] = {str(trait_item.get()): trait_item.get().current_value
for trait_item in obj.social_skills}
return character_skill_fields
class MageSerializer(GenericCharacterFieldMixin, serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.username')
arcana = serializers.SerializerMethodField()
def get_arcana(self, obj):
if obj:
return {str(arcana): arcana.current_value for arcana in obj.linked_arcana.all()}
class Meta:
model = Mage
fields = ('id', 'player', 'name', 'sub_race', 'faction', 'is_published',
'power_level', 'energy_trait', 'virtue', 'vice', 'morality', 'size',
'arcana', 'attributes', 'skills')
depth = 1
GenericCharacterFieldMixin is a Mixin of Fields for Characters, that are Generic, i.e. common to all types of characters.
I'd like my Mage Serializer to have these 'mixed in' rather than c/p then between all types of character (Mage is a type of character) hopefully this will increase DRYness in my webapp.
The issue is on the model I have this:
class NWODCharacter(models.Model):
class Meta:
abstract = True
ordering = ['updated_date', 'created_date']
name = models.CharField(max_length=200)
player = models.ForeignKey('auth.User', related_name="%(class)s_by_user")
....
def save(self, *args, **kwargs):
...
attributes = GenericRelation('CharacterAttributeLink')
skills = GenericRelation('CharacterSkillLink')
Which means I get this error:
TypeError at /characters/api/mages
<django.contrib.contenttypes.fields.create_generic_related_manager.<locals>.GenericRelatedObjectManager object at 0x00000000051CBD30> is not JSON serializable
Django Rest Framework thinks I want to serialize my generic relationship.
If I rename the fields in the model (s/attributes/foos/g, s/skills/bars/g) then I get a different (less clear?) error :
ImproperlyConfigured at /characters/api/mages
Field name `attributes` is not valid for model `ModelBase`.
How do I pull those methods and fields into a mixin, without confusing DRF?

Set SerializerMetaclass:
from rest_framework import serializers
class GenericCharacterFieldMixin(metaclass=serializers.SerializerMetaclass):
# ...
This is the solution recommended by DRF's authors.
Solutions suggested in the previous answers are problematic:
user1376455's solution hacks DRF into registering the mixin's fields in _declared_fields by declaring them on the child as different fields. This hack might not work in subsequent versions of the framework.
Nikolay Fominyh's solution changes the mixin to a fully fledged serializer (note that due to this, the name GenericCharacterFieldMixin is very unfortunate for a class which is not a mixin, but a serializer!). This is problematic because it takes the full Serializer class into the multiple inheritance, see the DRF issue for examples demonstrating why this is a bad idea.

Solution is simple as changing
class GenericCharacterFieldMixin():
to
class GenericCharacterFieldMixin(serializers.Serializer):

i had same issue and my google search brought me here. i managed to solve it.
since you are including attributes and skill fields in serialiser, you need to provide serialisation method for it.
this worked for me
class MageSerializer(GenericCharacterFieldMixin, serializers.ModelSerializer):
player = serializers.ReadOnlyField(source='player.username')
arcana = serializers.SerializerMethodField()
attributes = serializers.PrimaryKeyRelatedField(many=True,
read_only= True)
skills = serializers.PrimaryKeyRelatedField(many=True,
read_only= True)
def get_arcana(self, obj):
if obj:
return {str(arcana): arcana.current_value for arcana in obj.linked_arcana.all()}
class Meta:
model = Mage
fields = ('id', 'player', 'name', 'sub_race', 'faction', 'is_published',
'power_level', 'energy_trait', 'virtue', 'vice', 'morality', 'size',
'arcana', 'attributes', 'skills')
depth = 1

Related

DRF SerializerMethodField how to pass parameters

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

Django rest framework nested serializer create method

I have created a nested serializer, when I try to post data in it it keeps on displaying either the foreign key value cannot be null or dictionary expected. I have gone through various similar questions and tried the responses but it is not working for me. Here are the models
##CLasses
class Classes(models.Model):
class_name = models.CharField(max_length=255)
class_code = models.CharField(max_length=255)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.class_name
class Meta:
ordering = ['class_code']
##Streams
class Stream(models.Model):
stream_name = models.CharField(max_length=255)
classes = models.ForeignKey(Classes,related_name="classes",on_delete=models.CASCADE)
created_date = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.stream_name
class Meta:
ordering = ['stream_name']
Here is the view
class StreamViewset(viewsets.ModelViewSet):
queryset = Stream.objects.all()
serializer_class = StreamSerializer
Here is the serializer class
class StreamSerializer(serializers.ModelSerializer):
# classesDetails = serializers.SerializerMethodField()
classes = ClassSerializer()
class Meta:
model = Stream
fields = '__all__'
def create(self,validated_data):
classes = Classes.objects.get(id=validated_data["classes"])
return Stream.objects.create(**validated_data, classes=classes)
# def perfom_create(self,serializer):
# serializer.save(classes=self.request.classes)
#depth = 1
# def get_classesDetails(self, obj):
# clas = Classes.objects.get(id=obj.classes)
# classesDetails = ClassSerializer(clas).data
# return classesDetails
I have tried several ways of enabling the create method but like this displays an error {"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}. Any contribution would be deeply appreciated
This is a very common situation when developing APIs with DRF.
The problem
Before DRF reaches the create() method, it validates the input, which I assume has a form similar to
{
"classes": 3,
"stream_name": "example"
}
This means that, since it was specified that
classes = ClassSerializer()
DRF is trying to build the classes dictionary from the integer. Of course, this will fail, and you can see that from the error dictionary
{"classes":{"non_field_errors":["Invalid data. Expected a dictionary, but got int."]}}
Solution 1 (requires a new writable field {field_name}_id)
A possible solution is to set read_only=True in your ClassSerializer, and use an alternative name for the field when writing, it's common to use {field_name}_id. That way, the validation won't be done. See this answer for more details.
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
'classes_id',
)
extra_kwargs = {
'classes_id': {'source': 'classes', 'write_only': True},
}
This is a clean solution but requires changing the user API. In case that's not an option, proceed to the next solution.
Solution 2 (requires overriding to_internal_value)
Here we override the to_internal_value method. This is where the nested ClassSerializer is throwing the error. To avoid this, we set that field to read_only and manage the validation and parsing in the method.
Note that since we're not declaring a classes field in the writable representation, the default action of super().to_internal_value is to ignore the value from the dictionary.
from rest_framework.exceptions import ValidationError
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
def to_internal_value(self, data):
classes_pk = data.get('classes')
internal_data = super().to_internal_value(data)
try:
classes = Classes.objects.get(pk=classes_pk)
except Classes.DoesNotExist:
raise ValidationError(
{'classes': ['Invalid classes primary key']},
code='invalid',
)
internal_data['classes'] = classes
return internal_data
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'created_date',
)
With this solution you can use the same field name for both reading and writing, but the code is a bit messy.
Additional notes
You're using the related_name argument incorrectly, see this question. It's the other way around,
classes = models.ForeignKey(
Classes,
related_name='streams',
on_delete=models.CASCADE,
)
In this case it should be streams.
Kevin Languasco describes the behaviour of the create method quite well and his solutions are valid ones. I would add a variation to solution 1:
class StreamSerializer(serializers.ModelSerializer):
classes = ClassSerializer(read_only=True)
classes_id = serializers.IntegerField(write_only=True)
def create(self,validated_data):
return Stream.objects.create(**validated_data, classes=classes)
class Meta:
model = Stream
fields = (
'pk',
'stream_name',
'classes',
'classes_id',
'created_date',
)
The serializer will work without overriding the create method, but you can still do so if you want to as in your example.
Pass the value classes_id in the body of your POST method, not classes. When deserializing the data, the validation will skip classes and will check classes_id instead.
When serializing the data (when you perform a GET request, for example), classes will be used with your nested dictionary and classes_id will be omitted.
You can also solve this issue in such a way,
Serializer class
# Classes serializer
class ClassesSerializer(ModelSerializer):
class Meta:
model = Classes
fields = '__all__'
# Stream serializer
class StreamSerializer(ModelSerializer):
classes = ClassesSerializer(read_only=True)
class Meta:
model = Stream
fields = '__all__'
View
# Create Stream view
#api_view(['POST'])
def create_stream(request):
classes_id = request.data['classes'] # or however you are sending the id
serializer = StreamSerializer(data=request.data)
if serializer.is_valid():
classes_instance = get_object_or_404(Classes, id=classes_id)
serializer.save(classes=classes_instance)
else:
return Response(serializer.errors)
return Response(serializer.data)

DRF: Conditially change serializer

I'm using Django 3.0 and I have a serializer using django-rest-framework. Let's say that for example I have a Forum object. Each forum has an owner that is a user.
In my GET /forums/ endpoint, I'd like to just have the owner_id. However, in my GET /forums/<forum_id>/ endpoint I'd like to return the entire embedded object.
Is there any way to have one serializer support both of these scenarios? If not, I would hate to have to make two serializers just to support this.
class ForumSerializer(serializers.ModelSerializer, compact=True):
if self.compact is False:
owner = UserSerializer(source='owner', read_only=True)
else:
owner_id = serializers.UUIDField(source='owner_id')
...
How can I achieve this compact thing?
class Meta:
fields = [...]
read_only_fields = ['owner', 'owner_id']
You can add a SerializerMethodField like this:
class ForumSerializer(serializers.ModelSerializer):
owner = serializer.SerializerMethodField()
def get_owner(self, obj):
if self.context['is_compact'] == True:
return obj.owner.pk
else:
return UserSerializer(obj.owner).data
class Meta:
model = YourModel
fields = '__all__'
# Usage in view
serializer = ForumSerializer(context={'is_compact':True})
I am passing is_compact value through serializer's extra context.
create two serializer classes
class ForumSerializerId(ModelSerializer):
class Meta:
model = Forum
fields = ['forum_id']
class ForumSerializerDetail(ModelSerializer):
class Meta:
model = Forum
on your view.py
forums(request):
forum_list = Forum.objects.all()
forum_serializer = ForumSerializerId(forum_list,many=True)
return Response({"form":forum_serializer.data})
forum_detail(request,pk):
forum = get_object_or_404(Forum,pk)
forum_serializer = ForumSerializerDetail(forum)
return Response({"form":forum_serializer.data})

DRF: Serializer for heterogneous list of related models

Roughly said, I have the following schema in ORM:
class Page(models.Model):
title = models.CharField(max_length=255, null=False, blank=False)
#property
def content(self):
return [Video.objects.all()[0], Text.objects.all()[0], Video.objects.all()[1]]
and I have the following set of classes to support serialization for detailed view:
class ContentSerializer(serializers.ListSerializer):
class Meta:
model = ???
fields = '???'
class PageDetailSerializer(serializers.ModelSerializer):
content = ContentSerializer(many=True)
class Meta:
model = Page
fields = ('title', 'content', )
So I'm looking for a way to serialize that Page.content property - which is:
a list;
will contain heterogeneous data (combination of, let's say Video, Audio, Text and other models.
So I need somehow patch one of builtin serializers to iterate thru the list and check type of each object. And then decide how to serialize each one. E.g. I could prepare kind of dynamically created ModelSerializer with:
obj_type = type(obj)
class ContentModelSerializer(serializers.ModelSerializer):
class Meta:
model = obj_type
fields = '__all__'
serialized_obj = ContentModelSerializer(obj)
How could I implement that?
You can simply achieve this by overriding the to_representation method of Page serializer. like this:
class PageDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('title', 'content', )
def to_representation(self, instance):
ctx = super(PageDetailSerializer, self).to_representation(instance)
content = instance.content # property field of page, will return list of items
serialized_content = []
for c in content:
if type(c) == Video:
serialized_content.append({... serialized data of video type ..})
elif type(c) == ...
# other conditions here..
I had googled a lot before found the solution. This article has a reference to SerializerMethodField, which let you add custom handler for a field. And the final solution, which worked for me is:
class PageDetailSerializer(serializers.ModelSerializer):
_cache_serializers = {}
content = serializers.SerializerMethodField()
class Meta:
model = Page
fields = ('title', 'content', )
def _get_content_item_serializer(self, content_item_type):
if content_item_type not in self._cache_serializers:
class ContentItemSerializer(serializers.ModelSerializer):
class Meta:
model = content_item_type
exclude = ('id', 'page', )
self._cache_serializers[content_item_type] = ContentItemSerializer
return self._cache_serializers[content_item_type]
def get_content(self, page):
return [
self._get_content_item_serializer(type(content_item))(content_item).data for content_item in page.content
]

only show a certain serializer field in a sub-level (newbie)

I'm slowly learning how to work with rest framework and I'm stuck in one part I don't really understand(my english isn't great either). I have this api point: building for which I show some data on api/building/ but I want a certain field to appear only on api/building/1 (1=pk number) and I cannot figure this out how.
Here is my serializer code so far:
class FloorSerializer(serializers.ModelSerializer):
class Meta:
model = Floor
fields = ('number',
'title')
class BuildingSerializer(serializers.HyperlinkedModelSerializer):
location = serializers.CharField(source='location_address')
avg_temperature = serializers.SerializerMethodField('_get_avg_temperature')
avg_humidity = serializers.SerializerMethodField('_get_avg_humidity')
occupancy_level = serializers.SerializerMethodField('_get_occupancy_level')
floors = FloorSerializer(many=True, read_only=True)
class Meta:
model = Building
fields = ('pk',
'title',
'image_url',
'location',
'campus_name',
'avg_temperature',
'avg_humidity',
'occupancy_level',
'floors')
def _get_avg_temperature(self, obj):
# magia filtrului per buildingu asta.
temp = SensorData.objects.filter(sensor__room__floor__building__pk=obj.pk).filter(sensor__type='TP')\
.aggregate(Avg('value'))
return temp
def _get_avg_humidity(self, obj):
# magia filtrului per buildingu asta.
hum = SensorData.objects.filter(sensor__room__floor__building__pk=obj.pk).filter(sensor__type='HU')\
.aggregate(Avg('value'))
return hum
def _get_occupancy_level(self, obj):
ocup = randint(45, 65)
return ocup
the field in question is floors. I want to show it only on api/building/pk level and while I read the documentation it is not quite clear to me.
Here is a great answer demonstrating what you should do:
https://stackoverflow.com/a/22755648/2402929
In summary, you should create a serializer that will contain all your methods and fields you want in the list route (/api/building/), then extend that serializer, adding the additional fields you want in the detail routes (/api/building/:pk)
Example:
class BuildingSerializer(serializers.HyperlinkedModelSerializer):
# additional methods for fields that will be
# inherited in the DetailBuildingSerializer
class Meta:
model = Building
fields = ('pk',
'title',
'image_url',
'location',
'campus_name',
'avg_temperature',
'avg_humidity',
'occupancy_level',
)
class DetailBuildingSerializer(BuildingSerializer):
class Meta:
model = Building
fields = ('pk',
'title',
'image_url',
'location',
'campus_name',
'avg_temperature',
'avg_humidity',
'occupancy_level',
'floors'
)
Later on, separate serializers in your viewset (assuming you are using viewsets):
class BuildingViewset(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == 'list':
return serializers.BuildingSerializer
if self.action == 'retrieve':
return serializers.BuildingDetailSerializer

Categories

Resources