How to properly update a many to many nested serializer? - python

I have been able to replicate the create method to add the correct nested serializers in a POST request. However, I'm still having issues updating in a PUT or PATCH. When using a PUT or PATCH request and I pass the entire object data or the "brands" data, it will only update in the position it is passed. So if I have an object with 3 values:
"brands": [
{
"id": 1,
"name": "Brand 1 Test"
},
{
"id": 2,
"name": "Brand 2 Test"
},
{
"id": 3,
"name": "Brand 3 Test"
}
}
If I pass:
"brands": [
{
"id": 1,
"name": "Brand 1 Test"
},
{
"id": 2,
"name": "Brand 2 Test"
}
It will give me the same list of 3 brands. But if I do that in reverse order it will update and add the 3rd brand. I'm not sure what's causing it. Here's the code I have:
Models
class Brand(models.Model):
name = models.CharField(max_length=500)
class Incentive(models.Model):
name = models.CharField(max_length=500)
brands = models.ManyToManyField(Brand, related_name='incentives_brand')
start_dt = models.DateTimeField(auto_now_add=False, blank=True, null=True)
end_dt = models.DateTimeField(auto_now_add=False, blank=True, null=True)
Serializers
class BrandSerializer(serializers.ModelSerializer):
class Meta:
model = Brand
depth = 1
fields = ['id', 'name']
class IncentiveSerializer(serializers.ModelSerializer):
brands = BrandSerializer(many=True)
class Meta:
model = Incentive
fields = ['id', 'name', 'brands', 'start_dt', 'end_dt']
def create(self, validated_data):
brands = validated_data.pop('brands', [])
instance = Incentive.objects.create(**validated_data)
for brand_data in brands:
brand = Brand.objects.get(**brand_data)
instance.brands.add(brand)
return instance
def update(self, instance, validated_data):
brands = validated_data.pop('brands', [])
instance = super().update(instance, validated_data)
for brand_data in brands:
brand = Brand.objects.get(**brand_data)
instance.brands.add(brand)
return instance
I think the issue lies somewhere here. If any more code is needed please let me know(ex. views, urls). I'm guessing in the update I'm not properly emptying the list of brands. I just can't see it. Any help would be appreciated.

I think the clue here is that you do instance.brands.add, which does exactly that, adding. Not removing as you noticed :)
You also have a set.
So:
brand_objs = []
for brand_data in brands:
brand = Brand.objects.get(**brand_data)
brand_objs.append(brand)
instance.brands.set(brand_objs)
But the usage could differ, I can imagine that you'd also want to be able to just add one, or more, brands? But could use different end points for that?
Endpoints example
api/incentive/1/brands # get
api/incentive/1/brands # post, set brands?
api/incentive/1/brands/add # add one or more?
api/incentive/1/brands/remove # remove specific one or more?

Add instance.brands.clear() like so:
This will clear related brands so you can update them freshly.
def update(self, instance, validated_data):
brands = validated_data.pop('brands', None)
instance = super().update(instance, validated_data)
# The condition below will update brands only if brands were
# specified in the request body
if brands is not None:
instance.brands.clear() # Clear related brands
for brand_data in brands:
brand = Brand.objects.get(**brand_data)
instance.brands.add(brand)
return instance

Related

Django REST - hide deserialized data

I would like to store some data in one of my database field. The data is added to that field while deserialization with POST method. Later when I want to show data with GET method I don't want that one field to be presented.
When I do POST I deserialize that string:
{
"car_id": 3,
"rating": 3
}
Later in views.py I do the deserialization while POST:
#api_view(['POST'])
def car_rate(request):
if request.method == 'POST':
rate_data = JSONParser().parse(request)
rate_serializer = CarRateSerializer(data=rate_data)
if rate_serializer.is_valid():
try:
car_obj = Car.objects.get(pk=rate_data['car_id'])
except Car.DoesNotExist:
return JsonResponse({'message': 'The car with given ID does not exist!'}, status=status.HTTP_404_NOT_FOUND)
# check if rate is from 1 to 5
r = rate_serializer.validated_data['rating']
if int(r) >= 1 and int(r) <= 5:
rate_serializer.save()
return JsonResponse({'message':'The rate is in the scope!'})
else:
return JsonResponse({'message':'The rate is NOT in the scope!'})
return JsonResponse(rate_serializer.errors)
And there is my models.py:
class Car(models.Model):
make = models.CharField(max_length=15)
model = models.CharField(max_length=15)
avg_rating = models.FloatField(default=0)
def __str__(self): # print it when Car instance is needed
return self.make
class CarRate(models.Model):
car_id = models.ForeignKey(Car, related_name='rates',
on_delete=models.CASCADE,
default=0)
rating = models.PositiveIntegerField(default=0)
The code does works (somehow). For now there can be added rates for one car (multiple rates) with POST moethods. I store the rates in CarRate class and later it will be used to calculate the average rate for a car. I just simply don't want to print it out with GET.
This is my output right now:
{
"id": 2,
"make": "Volkswagen",
"model": "Golf",
"rates": [
4,
4,
2,
3
],
"avg_rating": 0.0
},
I simply want the rates field to be invisible while printing.
I read about defer() method and tried it out, but nothing happened. Any help?
If you absolutely don't want that field to be in your database ever, then you can simply remove that field from the Serializer field option (You named that CarRateSerializer)
But if you want that to be in your database but you don't want that to show as output, you can use extra_kwargs with 'write_only': True in your serializer class. I'm giving you an example I used for one of my projects
class TopicSerializer(serializers.ModelSerializer):
class Meta:
model = Topic
fields = ['id','title', 'totalMarks', 'status', 'categoryID']
extra_kwargs = {'categoryID': {'write_only': True}}
for your code, you can add this line of code below fields in that class Meta of your CarRateSerializer
extra_kwargs = {'rating': {'write_only': True}}
I hope this should solve your issue
Just remove rating field from, CarRateSerializer OR you can create a new Serializer for CarRate.

Best way to modify the fields in response in django rest?

I have a serializer that gives this data
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('id', 'name', 'cost', 'currency')
class UserSerializer(serializers.ModelSerializer):
posts = PostSerializer(many=True)
class Meta:
model = User
fields = ('id', 'name')
and it gives the response like this,
{
"id": 1,
"name": "joe",
"posts": [
{
"id": 20,
"name": "first post",
"cost": 20.00,
"currency": "USD"
},
{
"id": 21,
"name": "second post",
"cost": 30.00,
"currency": "USD"
}
]
}
However I want to change/add the fields of the response based on few conditions,
Eg. if cost is less than 25, make it zero and add a discount field for every post.
This is how I am doing.
class MyPostView(APIView):
def get(request):
query_set = User.objects.all()
user_and_posts = UserSerializer(query_set)
response_data = user_and_posts.data
# I am modifying the serializer data here :<
for post in response_data['posts']:
post['discount'] = 10 # some value
if post['cost'] < 25:
post['cost'] = 0
return Response(serializer.data, status=status.HTTP_200_OK)
To me modifying the primitive data like this is not looking right,
is there any alternate way in django rest to do this?
or could've done better with serializer?
In general, what's the best way to alter the response data we get from serializer and
format it in the way client wants? In languages like Java, we will have serializer for model and another serializer for output.. Can I do something similar?
If it is something that is model related and can be derived by manipulating model variables, I would advise to add properties into your model
class Post(models.Model):
name = models.CharField(max_length=64)
cost = models.DecimalField(max_digits=8, decimal_places=2)
currency = models.CharField(max_length=3)
#property
def alternative_cost(self):
if self.cost < 25:
return 0
else:
return self.cost
Then you have to add the newly created property to serializer.

Django Rest Framework writtable nested serializer is missing m2m field content

I'm trying while creating object A, create object B, which are in many-to-many relation. I've read about it for some time already and I've created custom create() method in serializer to create associated B objects while creating A object, but there's no trace of B content in validated_data.
My models:
class A(models.Model):
name = models.CharField('Name', max_length=250)
description = models.TextField('Content')
timestamp = models.DateTimeField('Timestamp', auto_now_add=True)
b_field = models.ManyToManyField(B, blank=True, null=True, verbose_name='b', related_name='a')
class B(models.Model):
name = models.CharField('Name', max_length=250)
description = models.TextField('Description')
timestamp = models.DateTimeField('Timestamp', auto_now_add=True)
And A serializer:
class ASerializer(serializers.ModelSerializer):
b = BSerializer(many=True, required=False)
class Meta:
model = A
fields = '__all__'
read_only_fields = ['pk', 'timestamp']
def create(self, validated_data):
bs = validated_data.pop('b')
a = A.objects.create(**validated_data)
for b in bs:
B.objects.create(**b)
return a
I've been printing content of validated_data at the begginging of create() method and it looked like this:
{'name': 'test', 'description': 'none'}
So, no trace of B content. I've been testing with postman, sending something like this:
{
"name": "test",
"description": "none",
"b": [
{
"id": 1,
"name": "b test",
"description": "none b",
}
]
}
Okay, so I managed to solve it by myself (almost). drf-writtable-nested to the rescue.
First of, I changed my A serializer so it now inherits after WritableNestedModelSerializer from package mentioned above. Thanks to this, content of b is now visible in validated_data passed to create() method. Speaking of which, from what I have read (and what can be seen in the question itself), there's a need to create m2m objects in overwritten create() method. Mine looks like this now:
def create(self, validated_data):
bs_data = validated_data.pop('b')
a = self.Meta.model.objects.create(**validated_data)
for b_data in bs_data:
b = B.objects.create(**b_data)
a.b.add(b)
return a
There's still place to validate B instance in this serializer, but let's say, this solves my issue.

Django Model Foreign Key filtering

I need to filter some Models in Django and return them trough REST, but I have some difficulties. I have 4 Models connected with Foreign key's like so:
class Standort(models.Model):
name = models.CharField(max_length=40)
farbe = models.CharField(max_length=20, default="Black")
class Gruppe(models.Model):
standort = models.ForeignKey(Standort)
name = models.CharField(max_length=40)
class Person(models.Model):
name = models.CharField(max_length=40)
gruppe = models.ForeignKey(Gruppe, related_name='personen')
class Eintrag(models.Model):
person = models.ForeignKey(Person, related_name='eintrage')
typ = models.ForeignKey(Typ)
datum = models.DateField()
and Iam serializing them like so:
class EintragSerializer(serializers.ModelSerializer):
class Meta:
model = Eintrag
fields = ('datum', 'typ')
class PersonenSerializer(serializers.ModelSerializer):
eintrage = EintragSerializer(read_only=True, many=True)
class Meta(object):
model = Person
fields = ('id', 'name', 'eintrage')
class GruppenPersonenEintraegeSerializer(serializers.ModelSerializer):
personen = PersonenSerializer(read_only=True, many=True)
class Meta(object):
model = Gruppe
fields = ('id', 'name', 'personnel')
and my view looks like this:
class GruppenPersonenEintraege(APIView):
def get(self, request, standort, jahr):
gruppen = Gruppe.objects.filter(standort=standort)
serializer = GruppenPersonenEintraegeSerializer(gruppen, many=True)
return Response(serializer.data)
The result looks like this:
[
{
"id": 2,
"name": "2.Schicht",
"personen": [
{
"id": 1,
"name": "Rolf der Tester",
"eintrage": [
{
"datum": "2017-02-16",
"typ": 3
},
{
"datum": "2017-02-15",
"typ": 3
},
{
"datum": "2018-04-05",
"typ": 2
}
]
}
]
},
{
"id": 3,
"name": "Test",
"personen": []
}
]
This is totally fine, my Problem is when i also want to filter the year of "eintrage.datum"by adding: .filter(standort=standort, personen__eintrage__datum__year=2017)afterGruppe.objects. Then the entry with "id": 2 is repeated 3 times and the one with "id": 3 isn't displayed at all. how do i filter just the entry's of the second nested dict?
To avoid "id":2 repeated multi times, you can just add a list(set()) surround the filter queryset result, the django restful framework can also treat the list the same way as queryset. Also notice that in django orm, the hash of a model instance is the prime_key of in db, so that's why the set can work on queryset.
As for "id":3 not showing, I also have no ideas as you did, maybe double better check the db again. A little bit more info will be more helpful.

Django Rest Framework 3.0: Saving Nested, Many-To-One Relationship

I'm attempting to build a nested relationship using Django Rest Framework 3.0. I've created my serializers and have attempted to override the create() function. My models are defined as follows:
class Item(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
name = models.CharField(max_length=200)
description = models.CharField(max_length=1000)
categories = models.ManyToManyField(Category, null=True, blank=True)
class Price(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
item = models.ForeignKey(Item, related_name='prices')
name = models.CharField(max_length=100)
cost = models.FloatField()
As you'll note, I can have multiple prices for my items. My serializers are defined as follows:
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
owner = serializers.Field(source='owner.username')
exclude = ('user',)
class ItemSerializer(serializers.ModelSerializer):
prices = PriceSerializer(many=True, required=False)
categories = CategorySerializer(many=True, required=False)
class Meta:
model = Item
owner = serializers.Field(source='owner.username')
fields = ('id', 'name', 'description', 'prices', 'categories')
def create(self, validated_data):
user = validated_data.get('user')
# Get our categories
category_data = validated_data.pop('categories')
# Create our item
item = Item.objects.create(**validated_data)
# Process the categories. We create any new categories, or return the ID of existing
# categories.
for category in category_data:
category['name'] = category['name'].title()
category, created = Category.objects.get_or_create(user=user, **category)
item.categories.add(category.id)
item.save()
return item
When I try and POST a new item:
{
"name": "Testing",
"description": "This is a test",
"categories": [
{
"name": "foo"
},
{
"name": "bar"
}
],
"prices": [
{
"name": "Red",
"cost": 10
}
]
}
I get the following error:
{
"prices": [
{
"item": [
"This field is required."
]
}
]
}
Presumably because the Price serializer has no idea what the ID of the new item is. I've tried overriding this functionality in the create() function of my serializer, but it appears as though the serializer's validation is being hit before I have the opportunity to create the item and associate it with the price.
So - How do I create a new item, get the item ID, and then create each of the new prices?
The problem is that your PriceSerializer is looking for the item key because it is specified on the Price model. This isn't immediately obvious because you are using Meta.exclude instead of Meta.fields.
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
exclude = ('user',)
Is the same as writing
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
fields = ('id', 'item', 'name', 'cost', )
Which makes it very clear what your issue is. Because your item field on the model does not have empty=True (or null=True) set, Django REST Framework automatically generates it as a PrimaryKeyRelatedField with required=True. This is why you are getting the This field is required is required error, because Django REST Framework cannot automatically detect that it is coming from a parent serializer which already has that field.
You can get around this by removing the field from the serializer, as it doesn't appear to ever be needed.
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
fields = ('id', 'name', 'cost', )
This will no longer display the item field though, but I suspect this isn't actually an issue for you.

Categories

Resources