How to serialize multiples objects from a Django model and add dynamically computed data in outputed JSON for each object? - python-3.x

I'm porting a Laravel PHP code to Python Django/Django Rest Framework.
My endpoint will output JSON.
I need to output many objects, but I need to add extra computed values for each object.
How can I achieve this ?
For example, my model is :
from django.db import models
from rest_framework.serializers import ModelSerializer
class MyObject(models.Model):
name = models.CharField(max_length=255)
score = models.IntegerField()
class MyObjectSerializer(ModelSerializer):
class Meta:
model = MyObject
fields = ( 'name', 'score' )
I retrieve a queryset with MyObject.objects.all() (or with filter).
For each MyObject in my queryset, I compute an extra value, called 'stats', that I want to output in my JSON output.
For example, if I have 2 objects MyObject(name='foo',score='1') and MyObject(name='bar',score='2'), I will compute a stats value for each object.
And my JSON output should be like :
{
{
'name': 'foo',
'score': 1,
'stats': 1.2
},
{
'name': 'bar',
'score': 2,
'stats': 1.3
},
}
What is the cleanest way , if any to achieve this ?
I can have a loop for each MyObject, serialize each MyObject, one by one with a serializer, and create and update dictionary for this object adding 'stats' key.
I'm afaid about performance.
What if I compute stats value only for some objects, mixing 2 kind of output ?

You can use SerializerMethodField:
class MyObjectSerializer(ModelSerializer):
stat = SerializerMethodField()
class Meta:
model = MyObject
fields = ( 'name', 'score', 'stat' )
def get_stat(self, obj):
# obj is the model instance (it passes only one even if many=True)
# do calculations with obj and return the value
return None
If performance is a concern where stat field uses related/foreign key models, you can either use annotations or select_related/prefetch_related. Using annotation is more efficient but can get difficult to create depending on the requirement.
If it's possible to annotate you can use other serializer fields like:
class MyObjectSerializer(ModelSerializer):
stat = FloatField(read_only=True)
class Meta:
model = MyObject
fields = ( 'name', 'score', 'stat' )

Apart from what #kyell wrote, you can also create a property in models using #property decorator and return your calculated data, this property is always read only.

Related

Adding an extra field to serializers.ModelSerializer

I have the following serializer which does its job:
from rest_framework import serializers
class FavoriteRecordSerializer(serializers.ModelSerializer):
date = serializers.DateTimeField(format='%b %d, %y')
class Meta:
model = FavoriteRecord
fields = ['user', 'date', 'record']
For some reason, I want the serializer to add an extra field, something like is_favorite = True when I serialize a FavoriteRecord object. So, the resulting serialized object could look something like this:
{ user: 1,
date: April 28, 21,
record: 3,
is_favorite: true //this extra field is what I want
}
Is it possible? (I know it can be done from the view, but I am not allowed to change the view- all I can change is this serializer.)
You can use SerializerMethodField it's value is dynamically evaluated by the serializer itself and not the model. Using this method the value can be generated in the context of the current session.
class FavoriteRecordSerializer(serializers.ModelSerializer):
date = serializers.DateTimeField(format='%b %d, %y')
is_favorite = serializers.SerializerMethodField()
class Meta:
model = FavoriteRecord
fields = '__all__'
def get_is_favorite(self, instance):
return True
Any SerializerMethodField will look for a get_<field_name> method and use it as source.

Django queryset for Case object and When Object, ---

I would to get your help on the following :
I have two models Parent model and a Child Model:
class Rate(models.Model):
RATE_VOLTAGE_CHOICES = (
(BAJA_TENSION, "Low tension"),
(MEDIA_TENSION, "Mid tension"),
(ALTA_TENSION, "High tension")
)
rate_type = models.CharField(max_length=1, choices=RATE_TYPE_CHOICES, default="0")
class ParentRate(models.Model):
rate = models.ForeignKey(Rate, on_delete=models.SET_NULL, blank=True, null=True
)
so front-end wants to "filter" rate_type choice by Low tension, Mid Tension, and High tension as strings, they send strings
so I am trying to use When Case object so I can filter then:
rates = ParentRate.objects.annotate(rate_voltage=Case(
When(rate__rate_type=Rate.BAJA_TENSION, then=Value('Low tension')),
When(rate__rate_type=Rate.MEDIA_TENSION, then=Value('Mid tension')),
When(rate__rate_type=Rate.ALTA_TENSION, then=Value('High tension')),
default=Value('Low tension'),
)
)
but I am getting the following error:
django.core.exceptions.FieldError: Cannot resolve expression type, unknown output_field
As documented in Aggregate() expression
The output_field argument requires a model field instance, like
IntegerField() or BooleanField(), into which Django will load the
value after it’s retrieved from the database.
Note that output_field is only required when Django is unable to
determine what field type the result should be. Complex expressions
that mix field types should define the desired output_field. For
example, adding an IntegerField() and a FloatField() together should
probably have output_field=FloatField() defined.
In your case it would be CharField
rates = ParentRate.objects.annotate(rate_voltage=Case(
When(rate__rate_type=Rate.BAJA_TENSION, then=Value('Low tension')),
When(rate__rate_type=Rate.MEDIA_TENSION, then=Value('Mid tension')),
When(rate__rate_type=Rate.ALTA_TENSION, then=Value('High tension')),
default=Value('Low tension'),
output_field=CharField()
),
)
You need to add an output_field to your query like this example:
from django.db.models import fields
rates = ParentRate.objects.annotate(rate_voltage=Case(
When(rate__rate_type=Rate.BAJA_TENSION, then=Value('Low tension')),
When(rate__rate_type=Rate.MEDIA_TENSION, then=Value('Mid tension')),
When(rate__rate_type=Rate.ALTA_TENSION, then=Value('High tension')),
default=Value('Low tension'),
output_field=fields.CharField() # if the output field is a string
)
)
For more informations: Django docs: Aggregate() expressions

Can Marshmallow auto-convert dot-delimited fields to nested JSON/dict in combination with unknown=EXCLUDE?

In trying to load() data with field names which are dot-delimited, using unknown=INCLUDE auto-converts this to nested dicts (which is what I want), however I'd like to do this with unknown=EXCLUDE as my data has a lot of properties I don't want to deal with.
It appears that with unknown=EXCLUDE, this auto-conversion does not happen and the dot-delimited field itself is passed to the schema, which of course is not recognized. This is confirmed by not using the unknown= param at all, which raises a ValidationError.
Is it possible to combine unknown=EXCLUDE and still get nested data? Or is there a better way to deal with this situation?
Thanks in advance!
# using marshmallow v3.7.1
from marshmallow import Schema, fields, INCLUDE, EXCLUDE
data = {'LEVEL1.LEVEL2.LEVEL3': 'FooBar'}
class Level3Schema(Schema):
LEVEL3 = fields.String()
class Level2Schema(Schema):
LEVEL2 = fields.Nested(Level3Schema)
class Level1Schema(Schema):
LEVEL1 = fields.Nested(Level2Schema)
schema = Level1Schema()
print(schema.load(data, unknown=INCLUDE))
# prints: {'LEVEL1': {'LEVEL2': {'LEVEL3': 'FooBar'}}}
print(schema.load(data, unknown=EXCLUDE))
# prints: {}
print(schema.load(data))
# raises: marshmallow.exceptions.ValidationError: {'LEVEL1.LEVEL2.LEVEL3': ['Unknown field.']}

DRF ModelSerializer make all fields Read Only without specifying them explicitely

I was able to make read only model serializer, e.g.:
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = ['name', 'ratio']
read_only_fields = fields
However, I tend to add/remove fields to/from Foo frequently. It would be much easier not to update my serializer each time Foo is modified. The fields = '__all__' is very handy:
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = '__all__'
read_only_fields = fields
However, the read_only_fields does not accept __all__ as a valid option and raises this exception:
Exception Type: TypeError at /api/foo/
Exception Value: The `read_only_fields` option must be a list or tuple. Got str.
How could I mark all fields as read only without explicitely adding each field to read_only_fields list?
You can extend get_fields method like this:
def get_fields(self):
fields = super().get_fields()
for field in fields.values():
field.read_only = True
return fields

Can I use ModelSerializer (DRF) to move multiple fields to a JSON field in a CREATE method?

I'm building an API with the Django Rest Framework. The main requirement is that it should allow for the flexible inclusion of extra fields in the call. Based on a POST call, I would like to create a new record in Django, where some fields (varying in name and number) should be added to a JSON field (lead_request).
I doubt if I should use the ModelSerializer, as I don't know how to handle the various fields that should be merged into one field as a JSON. In the create method, I can't merge the additional fields into the JSON, as they aren't validated.
class Leads(models.Model):
campaign_id = models.ForeignKey(Campaigns, on_delete=models.DO_NOTHING)
lead_email = models.EmailField(null=True, blank=True)
lead_request = JSONField(default=dict, null=True, blank=True)
class LeadCreateSerializer(serializers.ModelSerializer):
def get_lead_request(self):
return {key: value for key, value in self.request.items() if key.startswith('rq_')}
class Meta:
model = Leads
fields = ['campaign_id',
'lead_email',
'lead_request']
def create(self, validated_data):
return Leads.objects.create(**validated_data)
The documentation mostly talks about assigning validated_data, but here that isn't possible.
If I understood correctly and you want to receive parameters through the URL as well, here's an example of how you could achieve what you want:
class LeadViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
data = request.data
lead_request = generate_lead_request(request)
data['lead_request'] = lead_request
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
...
And on generate_lead_request you could parse all the additional fields that may have been sent through request.data (body) as well as through the request.query_params.
If i understand the problem properly main obstruction here is we don't know the exact JSON data format of lead_request. I am thinking about two possible model of solution for this problem. I not sure either of them is appropriate or not. Just want to share my opinion.
case 1
Lets assume data passed to LeadCreateSerializer in this type of format
data = {
'campaign_id': campaign_id,
'lead_email': lead_email,
'lead_request': {
# lead_request
}
}
Then this is easy, normal model serializer should able to do that. If data is not in properly formatted and it possible to organize before passing to serializer that this should those view or functions responsibility to make it proper format.
case 2
Lets assume this is not possible to organize data before passing that in LeadCreateSerializer then we need to get our related value during the validation or get of lead_request. As this serializer responsibility is to create new instance and for that validate fields so we assume in self.context the whole self.context.request is present.
class LeadCreateSerializer(serializers.ModelSerializer):
def generate_lead_request(self, data):
# do your all possible validation and return
# in dict format
def get_lead_request(self):
request = self.context.request
lead_request = self.generate_lead_request(request.data)
return lead_request
class Meta:
model = Leads
fields = ['campaign_id',
'lead_email',
'lead_request']

Resources