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

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

Related

How can I filter DRF serializer HyperlinkedRelationField queryset based on request data?

I have my users account authority stored in request.session.get('authority')
At the moment the endpoint in DRFs web-browsable HTML representation is showing all the addresses of all accounts in the html form. Which I'd expect as I'm querying all the addresses.
Screenshot of DRF form: addresses are displaying their authority.uuid, they should only show the current authorities address data.
Company Serializer
# ToDo: filter queryset objects
class CompanySerializer(serializers.ModelSerializer):
clients = serializers.HyperlinkedRelatedField(
many=True,
view_name='client-detail',
queryset=Client.objects.all()
)
addresses = serializers.HyperlinkedRelatedField(
many=True,
view_name='address-detail',
queryset=Address.objects.all()
)
class Meta:
model = Company
fields = ('name', 'url', 'clients', 'addresses')
read_only_fields = ('authority',)
I want to be able to do something like:
addresses = serializers.HyperlinkedRelatedField(
many=True,
view_name='address-detail',
queryset=Address.objects.filter(authority=request.session.get('authority'))
)
But not sure there is a way to access the request data in the serializer when I'm setting up the HyperlinkedRelatedField.
Perhaps I'm approaching this in entirely the wrong way. Any guidance will be greatly appreciated.
Update
Many thanks to Enthusiast Martin, based on his answer this is how I've implemented it for now:
def hyperlinkedRelatedFieldByAuthority(model, view_name, authority):
return serializers.HyperlinkedRelatedField(
many=True,
view_name=view_name,
queryset=model.objects.filter(authority=authority)
)
class CompanySerializer(serializers.ModelSerializer):
def get_fields(self):
fields = super().get_fields()
authority = self.context['request'].session.get('authority')
fields['clients'] = hyperlinkedRelatedFieldByAuthority(Client, 'client-detail', authority)
fields['addresses'] = hyperlinkedRelatedFieldByAuthority(Address, 'address-detail', authority)
return fields
You can override serializer's get_fields method and update the addresses queryset.
You can access the request via serializer's context
Something like this:
def get_fields(self):
fields = super().get_fields()
request = self.context['request']
fields['addresses'].queryset = ** your queryset using request data **
return fields

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 Rest Framework update nested serializer

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.

Django Rest Framework: How to save a field as an object of a different model, then return the key to the original model

Sorry for the weird title.
I have 2 models:
class TranslationWord(models.Model):
translation = models.TextField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class TranslationByUser(models.Model):
synset = models.ForeignKey(Synset)
user = models.ForeignKey(User)
translation = models.ForeignKey(TranslationWord)
The first one is supposed to basically just save words. The second is supposed to get a user's input. If the word exists in the first class, the foreign key value is simply stored. If it doesn't exist, I want to first create an instance of the TranslationWord, and then add the foreign key to the second class.
I'm doing all this with the Django Rest Framework, so I'm pretty stumped.
Currently, I've got these 2 models, 2 serializers (both just instances of ModelSerializer), and a view to save it (ListCreateAPIView).
How should I go about doing this?
These are basically the steps for creating a successfully validated object in a ModelViewSet create method (it's defined in CreateModelMixin):
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
It means you can override pre_save for your action in your ViewSet for TranslationByUser, setting attributes as side-effects in the object:
def pre_save(self, obj):
#get your_translation_word from obj or self.kwargs
your_translation_word = get_translation_word()
translation = TranslationWord(translation=your_translation_word)
translation.save()
setattr(obj, 'translation', translation)
#if you also want to support Update, call super method
super(TranslationByUserViewSet, self).pre_save(obj)
Another thing you can try is to define TranslationWordSerializer as a nested field in TranslationByUserSerializer. This topic is explained in the docs. Not sure if DRF handles everything about the creation though. I've only tested this behaviour with Multi-table Inheritance (and it works).
Anyway, for anyone who is curious, I created a write-only field in the Serializer, and used it to create the instance in the restore_object method.
class MySerializer(serializers.ModelSerializer):
user = UserSerializer(required=False)
translation = TranslationLemmaSerializer(required=False)
translation_text = serializers.WritableField(required=False, write_only=True)
class Meta:
model = TranslationByUser
fields = ('id','user','synset', 'translation', 'translation_text',)
read_only_fields = ('id', 'created_at', 'updated_at',)
def restore_object(self, attrs, instance=None):
print attrs
if instance is not None:
instance.synset = attrs.get('synset', instance.synset)
return instance
translation_text = attrs.get('translation_text')
del attrs['translation_text'] #delete non-model attribute before actually creating it
translationHIT = TranslationByUser(**attrs) #create model here
translation_id = None
try:
translation_instance = TranslationWord.objects.get(translation=translation_text) #check if translationWord is already present
except:
translation_instance = TranslationWord(translation=translation_text)
translation_instance.save() #otherwise, create it
TranslationByUser.translation = translation_instance
print attrs
return TranslationByUser
def get_validation_exclusions(self,instance=None):
exclusions = super(MySerializer, self).get_validation_exclusions()
return exclusions + ['user', 'translation']

Django REST framework and creating two table entries from a POST form

I want to create entries in two tables (Log and Post) using the DRF Browseable API POST form.
The example below is contrived, but it outlines what I am trying to do.
class Post(models.Model):
info = models.CharField()
class Log(TimeStampedModel):
ip = models.GenericIPAddressField(('IP Address'))
name = models.CharField()
data = models.ForeignKey(Post)
I want to use the browseable API to submit a form to create a Log entry. Here are the serializers:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('info',)
class LogSerializer(serializers.ModelSerializer):
data = serializers.Field()
class Meta:
model = Log
fields = ('ip', 'name', 'data')
The problem with the above is that serializer.Field is read only so does not show up on the POST form. If I change it to CharField it shows up, but then I get an error because an instance of a Post is expected not just a field of the Post object.
Here are my views:
class LogMixin(object):
queryset = Log.objects.all()
serializer_class = LogSerializer
class LogList(LogMixin, ListCreateAPIView):
pass
class LogDetail(LogMixin, RetrieveUpdateDestroyAPIView):
pass
What's the correct way of doing this?
From what I can tell you want to create a nested Log object. There are 2 ways of doing this:
Send 2 POST Requests, One to create the Post, and the other to create the Log contained the received HTTP 200 data from the API.
(Django and best way) Send the data all in one POST and parse it server side. Django Rest Framework takes care of this for you.
I have changed your code so that it should work.
Source
class LogSerializer(serializers.ModelSerializer):
class Meta:
model = Log
fields = ('ip', 'name')
class PostSerializer(serializers.ModelSerializer):
log = LogSerializer()
class Meta:
model = Post
fields = ('info', 'log')
views.py
import generics
class PostCreateAPIView(generics.CreateAPIView):
model = Post
serializer_class = PostSerializer
Then you can send a POST Request containing 'info', 'ip', and 'name'.
This is a hacky way and the best way to use the nested serializer as stated above. But just to show another way I am posting it here.
# Add New Item
#api_view(('POST',))
def add_new_item(request):
request.data['areaname_name'] = request.data['item_areaname']
request.data['buildingname_name'] = request.data['item_buildingname']
item_serializer = TblItemSerializer(data=request.data)
area_name_serializer = TblAreanameSerializer(data=request.data)
building_name_serializer = TblBuildingnameSerializer(data=request.data)
response = []
if item_serializer.is_valid() & area_name_serializer.is_valid() & building_name_serializer.is_valid():
# Save To Item Table
item_serializer.save()
# Save Unique Values Into Areaname and Buildingname Tables
area_name_serializer.save()
building_name_serializer.save()
return Response(item_serializer.data, status=status.HTTP_201_CREATED)
else:
response.append(item_serializer.errors)
response.append(area_name_serializer.errors)
response.append(building_name_serializer.errors)
return Response(response, status=status.HTTP_400_BAD_REQUEST)
In the error response you could also use (depending on how you want to handle on client side)
merge = {**item_serializer.errors, **area_name_serializer.errors, **building_name_serializer.errors}
return Response(merge, status=status.HTTP_400_BAD_REQUEST)