Django Rest Framework update nested serializer - django

I have a big misunderstanding with DRF nested serializers. I read docs about this and found out that I need to provide my own update method. So, here it is:
class SkillsSerializer(serializers.ModelSerializer):
class Meta:
model = Skills
class ProfileSerializer(serializers.ModelSerializer):
skills = SkillsSerializer(many=True)
class Meta:
model = Profile
fields = ('user', 'f_name', 'l_name', 'bd_day', 'bd_month', 'bd_year', 'spec', 'company', 'rate', 'skills', 'bill_rate', 'website', 'about', 'city', 'avatar', 'filled')
def update(self, instance, validated_data):
instance.user_id = validated_data.get('user', instance.user_id)
instance.f_name = validated_data.get('f_name', instance.f_name)
instance.l_name = validated_data.get('l_name', instance.l_name)
instance.bd_day = validated_data.get('bd_day', instance.bd_day)
instance.bd_month = validated_data.get('bd_month', instance.bd_month)
instance.bd_year = validated_data.get('bd_year', instance.bd_year)
instance.spec = validated_data.get('spec', instance.spec)
instance.company = validated_data.get('company', instance.company)
instance.rate = validated_data.get('rate', instance.rate)
instance.website = validated_data.get('website', instance.website)
instance.avatar = validated_data.get('avatar', instance.avatar)
instance.about = validated_data.get('about', instance.about)
instance.city = validated_data.get('city', instance.city)
instance.filled = validated_data.get('filled', instance.filled)
instance.skills = validated_data.get('skills', instance.skills)
instance.save()
return instance
I compared it with docs and didn't found any difference. But in this case, when I try to update skills, it doesn't work. And there is a real magic: when I put this
instance.skills = validated_data.get('bd_day', instance.skills)
It works PERFECTLY WELL! For ex., if I put bd_day = 12, update method saves instance with skills with ID's 1 and 2.
So, it seems like serializer ignores skills from AJAX data and still thinking, that skills serializer is read_only.
So, what is a point of this logic and how I can finally update my skills?
UPDATE
My models:
class Skills(models.Model):
tags = models.CharField(max_length='255', blank=True, null=True)
def __unicode__(self):
return self.tags
class Profile(models.Model):
user = models.OneToOneField(User, primary_key=True)
...
skills = models.ManyToManyField(Skills, related_name='skills')
...
UPDATE2
Still doesn't have any solution for this case! I tried this and this - the same result.
It seems that serializer ignored JSON data at all.

I had to update the answer, you did it inefficient way, so see the solution, it's far better
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('user', 'f_name', ... 'skills', ... 'filled')
depth = 1
class ProfileUpdateSerializer(serializers.ModelSerializer):
skills = serializers.PrimaryKeyRelatedField(many=True, queryset=Skills.objects.all(), required=False)
class Meta:
model = Profile
fields = ('user', 'f_name', ... 'skills', ... 'filled')
def update(self, instance, validated_data):
user = validated_data.pop('user', {})
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
if user:
User.objects.filter(id=self.context['request'].user.id).update(**user)
return instance
But after that I had another issue. I can received only one element from array of skills. And I found solution here:
$.ajax({
url: myurl,
type: 'PUT',
dataType: 'json',
traditional: true,<-----THIS!
data: data,
And that's it! It works like a charm!
I hope, my solution will be useful!

You have an issue here as you're providing non model data.
this:
instance.skills = validated_data.get('skills', instance.skills)
Will not provide Skill model instances but a dictionary.
You need to get the skills instance first and then inject them back to the instance.skills.

Related

How can I serialize a Many to Many field with added data in Django Rest Framework, without extra keys in the response?

I am using Django 3.2 and Django Rest Framework 3.14.
I have Users that should be able to follow other Users, like on a social networking site. When serializing a full User, I would like to get a list of those following Users, as well as additional data, such as the follow datetime. However, I would like the response to stay "flat", like this:
{
"username":"admin",
"followers":[
{
"username":"testuser",
--additional user fields--
"follow_date":"2023-02-08 01:00:02"
},
--additional followers--
],
--additional user fields--
}
I can only seem to go "through" my join model using an extra serializer, and end up with this:
{
"username":"admin",
"followers":[
{
"user":{
"username":"testuser",
--additional user fields--
},
"follow_date":"2023-02-08 01:00:02"
},
--additional followers--
],
--additional user fields--
}
Note how there is an additional user key
"user":{
"username":"testuser",
--additional user fields--
},
I don't want that there!
My model looks something like this:
class Follower(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.RESTRICT, related_name="followers")
follower = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.RESTRICT, related_name="following")
follow_date = models.DateTimeField(auto_now_add=True)
My serializers look like this (extra fields trimmed for brevity):
class UserSnippetSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username']
class FollowerSerializer(serializers.ModelSerializer):
user = UserSnippetSerializer(source='follower')
class Meta:
model = Follower
fields = ['user', 'follow_date']
class FullUserSerializer(serializers.ModelSerializer):
followers = FollowerSerializer(source='user.followers', many=True)
following = FollowerSerializer(source='user.following', many=True)
class Meta:
model = Profile
fields = ['username', 'followers', 'following']
Given this, I understand why it's doing what it is. But I can't seem to find any way to skip the extra user key, while still including bot the User information AND the follow_date.
I've tried playing around with proxy models on User, but that didn't seem to help. I suspect that this is no configuration thing, though I would like it to be, and that I need to override some internal function. But I can't seem to locate what that would be.
What's the best (and hopefully easiest!) way to accomplish this?
One approach is to use a serializer method field on FollowerSerializer like this:
class FollowerSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField()
class Meta:
model = Follower
fields = ['username', 'follow_date']
def get_username(self, obj):
return obj.follower.username
or if you have lots of fields to display from user, you can also override to_representation like this:
class FollowerSerializer(serializers.ModelSerializer):
user = UserSnippetSerializer(source='follower')
class Meta:
model = Follower
fields = ['user', 'follow_date']
def to_representation(self, instance):
data = super().to_representation(instance)
user_data = data.pop("user")
# flatten the follower data with the user data
return {
**data,
**user_data,
}
Note that this will also affect FullUserSerializer.following

How to response different data that was inserted by create method (ModelViewSet)

I'm implementing a stock shoes manager with REST architecture using Django + Django rest.
Im using a custom Router inherited from DefaultRouter to serve my endpoints.
In the /resources/id endpoint Ive added one more verb, POST that is called by custom_create method.
Here you can see this custom_create method:
viewsets.py
class ShoeViewSet(viewsets.ModelViewSet):
queryset = Shoe.objects.all()
filter_class = ShoeFilter
def get_serializer_class(self):
if self.action == 'custom_create':
return StockPostSerializer
else:
return ShoeSerializer
def custom_create(self, request, *args, **kwargs):
data = {}
data['shoe'] = kwargs['pk']
data['size'] = request.data.get('size')
data['amount'] = request.data.get('amount')
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
I needed to do this because I have two models, below you can see my 3 Serializers:
serializers.py
class StockSerializer(serializers.ModelSerializer):
class Meta:
model = Stock
fields = ['size', 'amount']
class ShoeSerializer(serializers.ModelSerializer):
stock = StockSerializer(many=True, read_only=True)
class Meta:
model = Shoe
fields = ['description', 'provider', 'type', 'cost_price','sale_price','total_amount', 'stock']
class StockPostSerializer(serializers.ModelSerializer):
class Meta:
model = Stock
fields = ['shoe','size', 'amount']
The retrieve (GET verb) method of this endpoint expects data serialized by ShoeSerializer, but the custom_create method insert data using the StockPostSerializer. How can I return a response with a different data that was inserted ?
When I try to insert with this endpoint I recieve this error message, but when I refresh the page I realize that the content was inserted (If i use postman instead of de DRF frontend I dont get any error message, works fine).
How can my custom_create method Responses correctly ?
You can check my github, the names will be a bit different because I translated it here so that it is easier for you to understand.
PS: As you may have noticed I am not a native speaker of the English language, so it is very difficult to express myself here but I am trying my best, and learning more and more. If my question contains grammar / concordance errors please correct them but you do not have to refuse me so I'm trying to learn!
I finally managed to sort this out, and in a much more elegant way than I had been trying beforehand.
What I need to do is: add new stock instances, for this I had created a new route for POST in the endpoint resources/id.
So I was able to reuse the Default Router, delete the custom_create method, and just modified the serializers.py file.
It looks like this:
serializers.py
class StockSerializer(serializers.ModelSerializer):
class Meta:
model = Stock
fields = ['size', 'amount']
class ShoeSerializer(serializers.ModelSerializer):
stock = StockSerializer(many=True)
def update(self, instance, validated_data):
instance.description = validated_data.get(
'description', instance.description)
instance.provider = validated_data.get(
'provider', instance.provider)
instance.type = validated_data.get('type', instance.type)
instance.cost_price = validated_data.get(
'cost_price', instance.cost_price)
instance.salve_price = validated_data.get(
'sale_price', instance.sale_price)
stock = instance.stock.all()
stock_data = validated_data.get('stock', [])
for item_data in stock_data:
item_id = item_data.get('size', None)
if item_id is not None:
item_db = stock.get(size=item_id)
item_db.size = item_data.get('size', item_db.size)
item_db.amount = item_data.get('amount',item_db.amount)
item_db.save()
else:
Estoque.objects.create(
shoe = instance,
size = item_data['size'],
amount = item_data['amount']
)
instance.save()
return instance
class Meta:
model = Shoe
fields = ['_id','description', 'provider', 'type', 'cost_price','sale_price','total_amount', 'stock']
Now, via PATCH verb I can add new Stock instances and alter existing stock instances. Thank you for the support!
If I understood correctly by looking at your code, in this case specifically, you don't need the StockPostSerializer. You can acheive the result you want by changing StockSerializer as follows:
class StockSerializer(serializers.ModelSerializer):
class Meta:
model = Stock
fields = ['shoe', 'size', 'amount']
extra_kwargs = {'shoe': {'write_only': True}}
I greatly apologize if I misunderstood your question.
EDIT:
Forgot to say. Using this serializer you don't need any extra route on your ModelViewSet

Django Rest Framework: How to pass a list of UUIDs for a nested relationship to a serializer?

TL;DR: What could be the reason the incoming data for one of my serializers does not get processed?
I'm working on a serializer for a nested relationship. The serializer should get a list of UUIDs, so that I can make many to many relationships. Here is the model:
class Order(
UniversallyUniqueIdentifiable,
SoftDeletableModel,
TimeStampedModel,
models.Model
):
menu_item = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
custom_choice_items = models.ManyToManyField(CustomChoiceItem, blank=True)
price = models.ForeignKey(MenuItemPrice, on_delete=models.CASCADE)
amount = models.PositiveSmallIntegerField(
validators=[MinValueValidator(MINIMUM_ORDER_AMOUNT)]
)
Here is the data (my post body) with which I hit the route in my tests:
data = {
"checkin_uuid": self.checkin.uuid,
"custom_choice_items": [],
"menu_item": self.menu_item.uuid,
"price": self.menu_item_price.uuid,
"amount": ORDER_AMOUNT,
}
response = self.client.post(self.order_consumer_create_url, self.data)
Note that the empty list for custom_choice_items does not change anything. Even if I fill it with values the same error occurs. And last but not least here are the serializers:
class CustomChoiceItemUUIDSerializer(serializers.ModelSerializer):
"""Serializer just for the uuids, which is used when creating orders."""
class Meta:
model = CustomChoiceItem
fields = ["uuid"]
....
# The serializer that does not work
class OrderSerializer(serializers.ModelSerializer):
menu_item = serializers.UUIDField(source="menu_item.uuid")
custom_choice_items = CustomChoiceItemUUIDSerializer()
price = serializers.UUIDField(source="price.uuid")
wish = serializers.CharField(required=False)
class Meta:
model = Order
fields = [
"uuid",
"menu_item",
"custom_choice_items",
"price",
"amount",
"wish",
]
The problem is now, that when I leave out many=True, I get the error:
{'custom_choice_items': [ErrorDetail(string='This field is required.', code='required')]}
And If I set many=True I just simply don't get any data. By that I mean e.g. the value of validated_data["custom_choice_items"] in the serializers create() method is just empty.
What goes wrong here?
I even checked that the data is in the request self.context["request"].data includes a key custom_choice_items the way I pass the data to this view!
EDIT: Here is the data I pass to custom_choice_items:
data = {
“checkin_uuid”: self.checkin.uuid,
“custom_choice_items”: [{“uuid”: custom_choice_item.uuid}],
“menu_item”: self.menu_item.uuid,
“price”: self.menu_item_price.uuid,
“amount”: ORDER_AMOUNT,
}
self.client.credentials(HTTP_AUTHORIZATION=“Token ” + self.token.key)
response = self.client.post(self.order_consumer_create_url, data)
When you post data using the test api client, if the data contains nested structure you should use format=json, like this:
response = self.client.post(self.order_consumer_create_url, data, format='json')
Did you override .create method in the serializer? Something like this should work:
from django.db import transaction
class OrderSerializer(serializers.ModelSerializer):
# your fields and Meta class here
#transaction.atomic
def create(self, validated_data):
custom_choice_items = validated_data.pop('custom_choice_items')
order = super().create(validated_data)
order.custom_choice_items.add(*custom_choice_items)
return order
By the way you don't really need to define CustomChoiceItemUUIDSerializer if is just the primary key of that.

Django Admin override displayed field value

I have following model:
class Model(models.Model):
creator = models.ForeignKey(User,related_name='com_creator',on_delete=models.SET_NULL, blank=True, null=True)
username = models.CharField(max_length=60,default="")
created = models.DateTimeField(auto_now_add=True)
body = models.TextField(max_length=10000,default=" ")
subtype = models.CharField(_("SubType"),max_length=100)
typ = models.CharField(_("Type"),max_length=50)
plus = models.ManyToManyField(User,related_name='com_plus', verbose_name=_('Plus'), blank=True)
is_anonymous = models.BooleanField(_('Stay Anonymous'), blank=True, default=False)
The values in typ and subtype are codes, like: "6_0_p", (because the original values are ridiculosly long, so I use codes and a dict to translate to human readable form).
QUESTION: How can I intercept these values in django admin and translate them to human readable form?
This is what i tried so far:
class ModelAdmin(admin.ModelAdmin):
model = Model
extra = 1
exclude = ("username","creator","plus" )
readonly_fields = ('subtype','typ','is_anonymous','created')
fields = ('body','subtype','typ')
def typ(self, obj):
self.typ = "ppp"
obj.typ = "ppp"
return "pppp"
I tried returning the object, self, some other value. I also tried to set the value without using a callable just declaring "typ='xxx'". Nothing. I probably don't understand how this whole thing works ...
Any ideas will be appreciated.
You need to create a readonly_field which corresponds to your method name, then return anything from that method.
Documentation here:
https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#django.contrib.admin.ModelAdmin.readonly_fields
class MyAdmin(admin.ModelAdmin):
readonly_fields = ('my_custom_field',)
def my_custom_field(self, obj):
return 'Return Anything Here'
I think that your code doesn't work because the method name is the same as the field.
You have to change the name of field, like _typ in fields tuple, then add a method called _typ that return anything or some obj attr.
i.e:
class ModelAdmin(admin.ModelAdmin):
model = Model
extra = 1
exclude = ("username","creator","plus" )
readonly_fields = ('subtype','typ','is_anonymous','created')
fields = ('body','subtype','_typ')
def _typ(self, obj):
return "pppp"
In Django 4 and above you can add a custom (readonly) field easily, like this:
class FooAdmin(admin.ModelAdminAbstractAdmin):
readonly_fields = [
'_dynamic_field',
]
fieldsets = [
('Form', {
'fields': (
'_dynamic_field',
),
'classes': ('form-group',)
}),
]
def _dynamic_field(self, obj=None):
if obj is not None:
return 'Bar'
return "Foo"
# to change the label
_dynamic_field.__name__ = "Dynamic field label"
How you can see, the field needs to be in readonly_fields and fieldsets too.

GenericForeignKey, ContentType and DjangoRestFramework

I am working on an discussion app in Django, that has Threads, Posts, Replies and Votes. Votes uses Generic Foreign Keys and Content Types to ensure a user can only vote once on a specific Thread/Post/Reply.
Vote model looks like this:
VOTE_TYPE = (
(-1, 'DISLIKE'),
(1, 'LIKE'),
)
class Vote(models.Model):
user = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType,
limit_choices_to={"model__in": ("Thread", "Reply", "Post")},
related_name="votes")
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
vote = models.IntegerField(choices=VOTE_TYPE)
objects = GetOrNoneManager()
class Meta():
unique_together = [('object_id', 'content_type', 'user')]
Vote Serializer:
class VoteSerializer(serializers.ModelSerializer):
class Meta:
model = Vote
The view to handle a vote:
#api_view(['POST'])
def discussions_vote(request):
if not request.user.is_authenticated():
return Response(status=status.HTTP_404_NOT_FOUND)
data = request.DATA
if data['obj_type'] == 'thread':
content_type = ContentType.objects.get_for_model(Thread)
print content_type.id
info = {
'content_type': content_type.id,
'user': request.user.id,
'object_id': data['obj']['id']
}
vote = Vote.objects.get_or_none(**info)
info['vote'] = data['vote']
ser = VoteSerializer(vote, data=info)
if ser.is_valid():
print "Valid"
else:
pprint.pprint(ser.errors)
return Response()
request.DATA content:
{u'vote': -1,
u'obj_type': u'thread',
u'obj':
{
...
u'id': 7,
...
}
}
When I vote, Django Rest Framework serializer throws an error:
Model content type with pk 149 does not exist.
149 is the correct id for the ContentType for the Thread model, according to
print content_type.id
I'm pretty much at a loss at what could be causing this...
The issue is probably that you have a generic foreign key in there, which could be linked to any type of model instance, so there's no default way of REST framework determining how to represent the serialized data.
Take a look at the docs on GFKs in serializers here, hopefully it should help get you started... http://www.django-rest-framework.org/api-guide/relations#generic-relationships
If you're still finding it problematic then simply drop out of using serializers altogether, and just perform the validation explicitly in the view, and return a dictionary of whatever values you want to use for the representation.