Adding an extra field to serializers.ModelSerializer - python-3.x

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.

Related

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

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.

How to show select field with pre-selected data using django forms?

I've a django Form with 2 choices (yes and no), on my "create page" i can render the select field to save the data and it works just fine, when i try to use on the "edit page" the value is not pre-selected with the current value, how can i make the current value selected on the select input field?
The form:
class MyForm(forms.ModelForm):
choose = forms.BooleanField(
required=False,
widget=forms.Select(
choices=[(1, 'Yes'), (0, 'No')],
attrs={'class': 'form-control'}
)
)
class Meta:
model = MyModel
When i call the view to edit:
class MyUpdateView(
LoginRequiredMixin,
SuccessMessageMixin,
UpdateView,
):
model = MyModel
form_class = MyForm
template_name = "my/template.html"
success_url = reverse_lazy('my-url')
success_message = 'Updated!'
def get_object(self, queryset=None):
data = super(MyUpdateView, self).get_object()
if not data.user == self.request.user:
raise Http404
# data.choose is False
return data
The HTML input will be always "1" (Yes) even tough the current value is "0" (No)
The HTML:
{{ form.choose }}
The Model:
class MyModel(models.Model):
choose = models.BooleanField(
default=False,
verbose_name='Cumulativo'
)
add this to your MyUpdateView:
initial = { 'choose': 1 }
You are defining the custom field 'choose' in the form, which does not refer to MyModel field 'choose'. that's why you are always getting the first value 'Y' as default or the first value 'Y' in the dropdown.
If you want to refer to your model object, you can simply use self keyword in the form
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['choose'].widget.attrs['class'] = 'form-control'
class Meta:
model = MyModel
fields = ('__all__')

Python understanding classes and functions

New to python and have been working on improving my skills overall, however, I struggle with understanding classes and functions.
Why can or can't I do the following code below
class Person():
name = 'Tom'
age = 31
has_job = False
Person.name = 'Tom'
Person.age = 31
Person.has_job = False
print(Person.name, Person.age, Person.has_job)
compared to this
class Person():
def __init__(self, name, age, has_job):
self.name = name
self.age = age
self.has_job = has_job
p1 = Person('Tom', 31, False)
Is this just bad practice or is it something else entirely?
I don't think that writing a class like your first example would be very usefull, because the attributes remain the same for each instance.
That means that every Person will be called by default 'Tom', will have the age: 41 and "has_job" will be set to false.
In the second example you've got a specific constructor that will initialise those variables and that's going to be more usefull. There's only one problem: you forgot to put ":" after def __init__(self, name, age, has_job) .
Also be aware of the indentation.
Your code should look like this:
class Person():
def __init__(self, name, age, has_job):
self.name = name
self.age = age
self.has_job = has_job
p1 = Person('Tom', 31, False)
print(p1.name);
Python is white space sensitive. Unless you want to change the default values in you class you do not need to redefine them.
class Person():
name = 'Tom'
age = 31
has_job = False
'''
change these will change the class values
Person.name = 'Tom'
Person.age = 31
Person.has_job = False
'''
print(Person.name, Person.age, Person.has_job)
In the first section of your code you are trying to define class attributes. These are attributes that do not change between instances of your class. On the other hand if you define variables in the def init(self) method these are parameters you must pass when creating the class and will be unique to each instance of the class you create. These are called instance attributes.
class Person():
# these are class attributes.
name = 'Tom'
age = 31
has_job = False
class Person2():
def __init__(self, name, age, has_job)
# these are instance attributes
self.name = name
self.age = age
self.has_job = has_job
In your first code snippet you did not indent the classes attributes appropriately when you created the class. Check my example above to see how that would be done.
So in your case since each person will be a new instance of your Person class, you do not want to have name, age and has_job as class attributes since those are unique to every person you create. If you had those variables as class attributes then each person you create using your Person() class will have the same name, age, and has_job values.
If you created a class with class attributes and then changed the class attributes of the class instance every time it would not be pythonic. Rather you should create instances of the class with instance attributes.
I HIGHLY recommend watching Corey Shafer OOP tutorials on youtube as they cover all this extensively: https://www.youtube.com/watch?v=ZDa-Z5JzLYM&list=PL-osiE80TeTt2d9bfVyTiXJA-UTHn6WwU&index=40

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