Django Rest Framework - Read nested data, write integer - python

So far I'm extremely happy with Django Rest Framework, which is why I alsmost can't believe there's such a large omission in the codebase. Hopefully someone knows of a way how to support this:
class PinSerializer(serializers.ModelSerializer):
item = ItemSerializer(read_only=True, source='item')
item = serializers.IntegerSerializer(write_only=True)
class Meta:
model = Pin
with the goal
The goal here is to read:
{pin: item: {name: 'a', url: 'b'}}
but to write using an id
{pin: item: 10}
An alternative would be to use two serializers, but that looks like a really ugly solution:
django rest framework model serializers - read nested, write flat

Django lets you access the Item on your Pin with the item attribute, but actually stores the relationship as item_id. You can use this strategy in your serializer to get around the fact that a Python object cannot have two attributes with the same name (a problem you would encounter in your code).
The best way to do this is to use a PrimaryKeyRelatedField with a source argument. This will ensure proper validation gets done, converting "item_id": <id> to "item": <instance> during field validation (immediately before the serializer's validate call). This allows you to manipulate the full object during validate, create, and update methods. Your final code would be:
class PinSerializer(serializers.ModelSerializer):
item = ItemSerializer(read_only=True)
item_id = serializers.PrimaryKeyRelatedField(write_only=True,
source='item',
queryset=Item.objects.all())
class Meta:
model = Pin
fields = ('id', 'item', 'item_id',)
Note 1: I also removed source='item' on the read-field as that was redundant.
Note 2: I actually find it rather unintuitive that Django Rest is set up such that a Pin serializer without an Item serializer specified returns the item_id as "item": <id> and not "item_id": <id>, but that is beside the point.
This method can even be used with forward and reverse "Many" relationships. For example, you can use an array of pin_ids to set all the Pins on an Item with the following code:
class ItemSerializer(serializers.ModelSerializer):
pins = PinSerializer(many=True, read_only=True)
pin_ids = serializers.PrimaryKeyRelatedField(many=True,
write_only=True,
source='pins',
queryset=Pin.objects.all())
class Meta:
model = Item
fields = ('id', 'pins', 'pin_ids',)
Another strategy that I previously recommended is to use an IntegerField to directly set the item_id. Assuming you are using a OneToOneField or ForeignKey to relate your Pin to your Item, you can set item_id to an integer without using the item field at all. This weakens the validation and can result in DB-level errors from constraints being violated. If you want to skip the validation DB call, have a specific need for the ID instead of the object in your validate/create/update code, or need simultaneously writable fields with the same source, this may be better, but I wouldn't recommend anymore. The full line would be:
item_id = serializers.IntegerField(write_only=True)

If you are using DRF 3.0 you can implement the new to_internal_value method to override the item field to change it to a PrimaryKeyRelatedField to allow the flat writes. The to_internal_value takes unvalidated incoming data as input and should return the validated data that will be made available as serializer.validated_data. See the docs: http://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data
So in your case it would be:
class ItemSerializer(ModelSerializer):
class Meta:
model = Item
class PinSerializer(ModelSerializer):
item = ItemSerializer()
# override the nested item field to PrimareKeyRelatedField on writes
def to_internal_value(self, data):
self.fields['item'] = serializers.PrimaryKeyRelatedField(queryset=Item.objects.all())
return super(PinSerializer, self).to_internal_value(data)
class Meta:
model = Pin
Two things to note: The browsable web api will still think that writes will be nested. I'm not sure how to fix that but I only using the web interface for debug so not a big deal. Also, after you write the item returned will have flat item instead of the nested one. To fix that you can add this code to force the reads to use the Item serializer always.
def to_representation(self, obj):
self.fields['item'] = ItemSerializer()
return super(PinSerializer, self).to_representation(obj)
I got the idea from this from Anton Dmitrievsky's answer here: DRF: Simple foreign key assignment with nested serializers?

You can create a Customized Serializer Field (http://www.django-rest-framework.org/api-guide/fields)
The example took from the link:
class ColourField(serializers.WritableField):
"""
Color objects are serialized into "rgb(#, #, #)" notation.
"""
def to_native(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
def from_native(self, data):
data = data.strip('rgb(').rstrip(')')
red, green, blue = [int(col) for col in data.split(',')]
return Color(red, green, blue)
Then use this field in your serializer class.

I create a Field type that tries to solve the problem of the Data Save requests with its ForeignKey in Integer, and the requests to read data with nested data
This is the class:
class NestedRelatedField(serializers.PrimaryKeyRelatedField):
"""
Model identical to PrimaryKeyRelatedField but its
representation will be nested and its input will
be a primary key.
"""
def __init__(self, **kwargs):
self.pk_field = kwargs.pop('pk_field', None)
self.model = kwargs.pop('model', None)
self.serializer_class = kwargs.pop('serializer_class', None)
super().__init__(**kwargs)
def to_representation(self, data):
pk = super(NestedRelatedField, self).to_representation(data)
try:
return self.serializer_class(self.model.objects.get(pk=pk)).data
except self.model.DoesNotExist:
return None
def to_internal_value(self, data):
return serializers.PrimaryKeyRelatedField.to_internal_value(self, data)
And so it would be used:
class PostModelSerializer(serializers.ModelSerializer):
message = NestedRelatedField(
queryset=MessagePrefix.objects.all(),
model=MessagePrefix,
serializer_class=MessagePrefixModelSerializer
)
I hope this helps you.

Related

DRF multiple update with reduced database hits

I'm using DRF's example of multiple updates which works fine except every self.child.update is a separate update query to the database.
Is there a way I can rewrite this to call the updates as one query as a bulk update?
class BookListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
book_mapping = {book.id: book for book in instance}
data_mapping = {item['id']: item for item in validated_data}
ret = []
for book_id, data in data_mapping.items():
book = book_mapping.get(book_id, None)
ret.append(self.child.update(book, data))
return ret
class BookSerializer(serializers.Serializer):
id = serializers.IntegerField()
class Meta:
list_serializer_class = BookListSerializer
Django has a command bulk_update
Docs: https://docs.djangoproject.com/en/4.1/ref/models/querysets/#bulk-update
I'd write up a full example but I'm not sure what validated_data looks like
A few suggestions:
- Use ModelSerializer, as mentioned. It will handle updates/creates automatically and more efficiently.
- Use model validation (clean methods) instead of serializer validation for database-level validation.
- Use bulk_create for the creation case (instead of a loop).
- Use select_related/prefetch_related to reduce queries when fetching the instance.
- Use .update() or F-expressions to update fields, instead of re-getting the instance from the DB.
- Use partial=True on the serializer if you expect partial updates.

How to create a custom function inside django model?

I have a django model
class UserInfluencerGroupList(models.Model):
list_name = models.CharField(max_length=255)
influencers = models.ManyToManyField(Influencer, blank=True)
user = models.ForeignKey(MyUser, on_delete = models.CASCADE)
def __str__(self):
return self.list_name
and my views function is:
def get_lists(request,user_email):
"""Get all the lists made by user"""
try:
user_instance = MyUser.objects.get(email=user_email)
except MyUser.DoesNotExist:
return HttpResponse(json.dumps({'message':'User not found'}),status=404)
if request.method == 'GET':
influencers_list = UserInfluencerGroupList.objects.all().order_by('id').filter(user=user_instance)
influencers_list = serializers.serialize('json',influencers_list, fields =['id','influencers','list_name'], indent=2, use_natural_foreign_keys=True, use_natural_primary_keys=True)
return HttpResponse(influencers_list,content_type='application/json',status=200)
else:
return HttpResponse(json.dumps({'message':'No lists found'}), status=400)
Apart from the usual data from list I also want to calculate the total_followers, total_likes and total_comments of each influencer in the list. The influencer model has fields for total_likes, comments and followers.
How should I write a function to calculate and display it along with all the other data that the list is returning
You should consider to use Django Rest Framework if you want to return a json of your own choice or/and if you're about to create your own rest api.
Alternative is to create the json all manually, i.e build the dictionary and then use json.dumps.
(If you really want to go "manual" see answer Convert Django Model to dict)
The django serializers does not support what you want to do:
option for serializing model properties (won't fix)
Quote for not fixing:
"I'm afraid I don't see the benefit of what you are proposing. The
serialization framework exists for the easy serialization of Django
DB-backed objects - not for the arbitrary serialization of _any_
object, and derived properties like the ones you are highlighting as
examples don't add anything to the serialized representation of a
DB-backed object..."

How to automatically fill-in model fields in Django rest_framework serializer?

Let's assume I have a model like this:
class Data(models.Model):
a = models.CharField()
b = models.CharField()
c = models.IntegerField()
I would like to setup a serializer in such a way that it automatically fills in field c and it is not required for a POST. I tried to overwrite the create function of the serializer, but it doesn't work:
class DataSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Data
fields = ('a', 'b')
def create(self, validated_data, **kwargs):
Data.objects.c = 5
return Data.objects.create(**validated_data)
However, if I try this, I end up with an IntegrityError: NOT NULL constraint failed: model_data.c. What is the syntax that I have to use here?
EDIT: Updated formatting.
The reason you're getting the error because field c is not set to null = True - as such an error is raised at the validation stage even before the serializer hits the create method.
Bear in mind that the process goes like this:
Submit serializer data
field-level validation happens - this includes checks for null integrity, min/max length etc and also any custom field validations defined in def validate_<field_name>
object-level validation happens - this calls the def validate method
validated data is passed to the save method, depending on how you designed the serializer - it will save the instance, or route the data to either create or update
All of the info regarding this can be found in Django's and DRF's docs.
A few things to consider:
are you setting a global default for that field? If so, set the default in your models - c = models.IntegerField(default=a_number_or_a_callable_that_returns_an_integer)
do you intend to display the field? If so, include c in your fields and add one more Meta attribute - read_only_fields = ('c',)
If it's neither of the above, you might want to override the validate_c method
Apologies for the poor formatting, typing it on my phone - will update once I get to a computer
In your code Data.objects.c = 5 does nothing.
If you want to set this value yourself use validated_data['c'] = 5 or Data.objects.create(c=5, **validated_data) (just not both at the same time).
Rather than doing this in the serializer, there are hooks in the generic views that allow you to pass values to the serializer. So in your case you might have:
class DataViewSet(ModelViewSet):
# ...
def perform_create(self, serializer):
serializer.save(c=5)
See the "Save and deletion hooks" section here

Set foreignkey field based on request

I'm using the Django Rest Framework 3.2. I have a model "Report" with a ForeignKey to the model "Office".
I want to set the report.office based on the request.
This is how I did it (which I don't like, because it requires duplication):
In my view I differ between GET or POST/PUT/PATCH requests, where GET uses a nested Serializer representation, and POST/PUT/PATCH uses flat serializer representations.
Then I do in my view:
def perform_create(self, serializer):
serializer.save(office=Office.objects.get_only_one_from_request(self.request))
and in my FlatOfficeSerializer:
def validate(self, attrs):
if self.instance is not None:
instance = self.instance
instance_fields = instance._meta.local_fields
for f in instance_fields:
f = self.get_initial().get(f.name)
else:
instance = self.Meta.model(**attrs)
request = self.context.get('request')
instance.office = Office.objects.get_only_one_from_request(request)
instance.clean()
return attrs
It is clear that there is a duplication there and although it is working I don't like it. Without the duplication the validation fails (of course) on the instance.clean(), which calls the clean method of the model. I really need this, because my business logic is in the model's clean and save methods.

Returning extended fields in JSON

I have two tabels(Ingredient_Step and Ingredient) in on relation as you can see below:
Models.Py
class Ingredient_Step(models.Model):
ingredient = models.ForeignKey(Ingredient)
Step = models.ForeignKey(Step)
def __unicode__(self):
return u'{}'.format(self.Step)
class Ingredient(models.Model):
IngredientName = models.CharField(max_length=200,unique=True)
Picture = models.ImageField(upload_to='Ingredient')
def __unicode__(self):
return u'{}'.format(self.IngredientName)
In a function, i need serialize a JSON object from a query that returns from "Ingredient_step", but I need send the field "IngredientName", who comes from "Ingredient" table.
I try using "ingredient__IngredientName" but it fails.
Views.Py:
def IngredientByStep(request):
if request.is_ajax() and request.GET and 'id_Step' in request.GET:
if request.GET["id_Step"] != '':
IngStp = Ingredient_Step.objects.filter(Step =request.GET["id_Step"])
return JSONResponse(serializers.serialize('json', IngStp, fields=('pk','ingredient__IngredientName')))
How i can call extends field from a relation?
Thanks
This "feature" of Django (and many ORM's like SQLAlchemy) are called Lazy Loading, meaning data is only loaded from related models if you specifically ask for them. In this case, build your IngStp as a list of results, and make sure to access the property for each result before serializing.
Here's an example of how to do that: Django: Include related models in JSON string?

Categories