I am writing an api for an image labelling game using Django Rest Framework's APIView class.
The game implies that a user will see a picture (resource) and label it with one or more labels (tagging). Since the picture is sent an an object through the GET request and a label references the picture the label was posted for, I somehow need to access the id of the picture when storing a label in the database.
Currently, I am testing my POST request, which is pretty standard:
def post(self, request, *args, **kwargs):
tag_serializer = TagSerializer(data=request.data)
tagging_serializer = TaggingSerializer(data=request.data)
if tagging_serializer.is_valid(raise_exception=True):
tagging_serializer.save(tagging=request.data)
return Response({"status": "success", "data": tagging_serializer.data}, status=status.HTTP_201_CREATED)
else:
return Response({"status": "error", "data": tag_serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
I am handling most of the logic in the TaggingSerializer:
class TaggingSerializer(serializers.ModelSerializer):
tag = TagSerializer(required=False, write_only=False)
resource_id = serializers.PrimaryKeyRelatedField(queryset=Resource.objects.all(),
required=True,
source='resource',
write_only=False)
gameround_id = serializers.PrimaryKeyRelatedField(queryset=Gameround.objects.all(),
required=False,
source='gameround',
write_only=False)
user_id = serializers.PrimaryKeyRelatedField(queryset=CustomUser.objects.all(),
required=False,
source='user',
write_only=False)
class Meta:
model = Tagging
fields = ('id', 'user_id', 'gameround_id', 'resource_id', 'tag', 'created', 'score', 'origin')
depth = 1
def create(self, validated_data):
"""Create and return a new tagging"""
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
score = 0
tag_data = validated_data.pop('tag', None)
if tag_data:
tag = Tag.objects.get_or_create(**tag_data)[0]
validated_data['tag'] = tag
if not Tag.objects.all().filter(name=tag.name).exists():
score = 0
elif Tag.objects.all().filter(name=tag.name).exists():
score += 5
elif Tag.objects.all().filter(name=tag.name) in coordinated_gameround_tags:
score += 25
tagging = Tagging(
user=user,
gameround=validated_data.get("gameround"),
# resource=request.query_params['resource'].id,
resource=validated_data.get("resource"),
tag=validated_data.get("tag"),
created=datetime.now(),
score=score,
origin=""
)
tagging.save()
return tagging
def to_representation(self, instance):
rep = super().to_representation(instance)
rep['tag'] = TagSerializer(instance.tag).data
return rep
I have tried accessing the resource from the get request with request.query_params but this also does not seem to work. When testing my post request in Postman I keep getting the error "resource_id": ["This field is required"].
Am I using request.query_params the wrong way? Is there another way to access this particular object that I don't know of? How else can I solve this?
Related
i set my post model with choice and migrate model.
after that i create postserializer for create
and i run the server and post with data in postman for test,
but i got the 'is not a valid choice' err on serializer.
here is my model,
class Post(models.Model):
CATEGORY_CHOICES = (
('mc', 'MIRACLE'),
('hw', 'HOMEWORK')
)
title = models.CharField(max_length=50)
category = models.CharField(max_length=2, choices=CATEGORY_CHOICES)
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
serializer,
class PostCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__'
view
class PostAPI(APIView):
def post(self, request):
data = {
'title' :request.data['title'],
'category':request.data['category'],
'content' :request.data['content'],
'author' :request.user.id
}
serializer = PostCreateSerializer(data=data)
if serializer.is_valid():
serializer.save()
else:
print(serializer.erros)
...
and request data (in Postman)
{
'title':'test_title',
'category':'HOMEWORK',
'content':'test_content'
}
result is
{'category': [ErrorDetail(string='"HOMEWORK" is not a valid choice.', code='invalid_choice')]}
i tried request with changing category 'HOMEWORK' to 'hw'
then it works
but i want request with large one
Your error on serialize level. Try this
readable_to_choice = {"HOMEWORK": "hw", "MIRACLE": "mc"}
data = {'title' :request.data['title'],
'category': readable_to_choice.get(request.data['category']),
'content' :request.data['content'],
'author' :request.user.id}
serializer = PostCreateSerializer(data=data)
UPD: you should make your api func like this:
serializer = PostCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
print(serializer.data) # do logic with that data
and try to override to_internal_value method in your serializer. for example:
def to_internal_value(self, data):
readable_to_choice = {"HOMEWORK": "hw", "MIRACLE": "mc"}
data["category"] = readable_to_choice.get(data["category"])
res = super().to_internal_value(data)
return res
I think this way is more accurate
Groom can like Bride, and Bride can like Groom. These two are different models in Likes Model.
I'm using the view set. Able to save individual model data but when I save Likes DRF throws field required error. Please suggest how to do better handling of many to many relationship handling between two models.
I also tried with ForeignKey relation in the models. The from bride/groom would be enforced with logged-in user id that is register bride/groom id (pk).
Models
class RegisterBride(models.Model):
'''Register Bride/Female profiles.'''
fname = models.CharField(max_length=200)
lname = models.CharField(max_length=200)
mobile = models.IntegerField(default=None)
gender = models.CharField(max_length=30, default='Female')
age = models.PositiveIntegerField(default=None, validators=[ MinValueValidator(18), MaxValueValidator(100)])
bride_account_status = models.BooleanField(default=False)
def __str__(self):
return self.fname + ' '+self.lname
class RegisterGroom(models.Model):
fname = models.CharField(max_length=200)
lname = models.CharField(max_length=200)
mobile = models.IntegerField(default=None)
gender = models.CharField(max_length=30, default='Male')
age = models.PositiveIntegerField(default=None, validators=[ MinValueValidator(18), MaxValueValidator(100)])
groom_account_status = models.BooleanField(default=False)
def __str__(self):
return self.fname + ' '+self.lname
class Likes(models.Model):
bride_profile = models.ManyToManyField(RegisterBride, related_name='bride_profile')
groom_profile = models.ManyToManyField(RegisterGroom, related_name='groom_profile')
likes = models.BooleanField(default=False)
created_date = models.DateField(auto_now_add=True)
Views
class RegisterGroomProfileViewSet(viewsets.ModelViewSet):
queryset = RegisterGroom.objects.all()
serializer_class = RegisterGroomSerializer
class RegisterBrideProfileViewSet(viewsets.ModelViewSet):
queryset = RegisterBride.objects.all()
serializer_class = RegisterBrideSerializer
class LikesViewSet(viewsets.ModelViewSet):
queryset = Likes.objects.all()
serializer_class = LikesSerializer
Serializers
class RegisterGroomSerializer(serializers.ModelSerializer):
class Meta:
model = RegisterGroom
fields = '__all__'
class RegisterBrideSerializer(serializers.ModelSerializer):
class Meta:
model = RegisterBride
fields = '__all__'
class LikesSerializer(serializers.ModelSerializer):
groom_profile_s = RegisterGroomSerializer(many=True, required=True)
bride_profile_s = RegisterBrideSerializer(many=True, required=True)
class Meta:
model = Likes
fields = '__all__'
extra_kwargs = {'likes': {'required': True}}
def create(self, data):
if data['bride_profile'] == data['groom_profile']:
raise ValidationError(detail='To Profile ID == From Profile ID. They should be different.')
return data
Request
Request URL: http://127.0.0.1:8000/likes/
Request Method: POST
Status Code: 400 Bad Request
{'likes': True,
'bride_profile': 1,
'groom_profile': 6}
Response
POST /likes/
HTTP 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"groom_profile_s": [
"This field is required."
],
"bride_profile_s": [
"This field is required."
]
}
Server Error:
..\rest_framework\pagination.py:200: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'register.models.Likes'> QuerySet.
paginator = self.django_paginator_class(queryset, page_size)
Please suggest me.
Your request is not good : you put groom_profile_s and bride_profile_s as required but you do not send them in your request !
Request URL: http://127.0.0.1:8000/likes/
Request Method: POST
Status Code: 400 Bad Request
{'likes': True,
'groom_profile_s': [1],
'bride_profile_s': [6]}
After modifying your query, you should update your serializer.
class LikesSerializer(serializers.ModelSerializer):
groom_profile_s = RegisterGroomSerializer(many=True, required=True)
bride_profile_s = RegisterBrideSerializer(many=True, required=True)
class Meta:
model = Likes
fields = '__all__'
extra_kwargs = {'likes': {'required': True}}
def create(self, data):
if 'groom_profile_s' in data.keys():
groom_profile_s = validated_data.pop('groom_profile_s')
// Deal with your data here by creating the Like
// Same for bride_profile_s
I'm creating a custom create method to process incoming data. It gets validated by a serializer:
# serializers.py:
class ItemVersionSerializer(serializers.ModelSerializer):
item_ver_id = serializers.RegexField(regex='^r\d{2}$', allow_blank=False)
session_id = serializers.RegexField(regex='^s\d{2}$', allow_blank=False)
config = serializers.CharField(min_length=6)
item_ver = serializers.CharField(min_length=6)
(...)
The method itself looks like this:
# views.py:
class ItemVersionViewSet(viewsets.ModelViewSet):
serializer_class = ItemVersionSerializer
lookup_field = 'item_ver_id'
def create(self, request, *args, **kwargs):
data = request.data
model = ItemModel.objects.get(item_id=data["model"])
finished = True if "finished" in data else False
item_version = ItemVersion.objects.create(
model=model,
item_ver_id=data["item_ver_id"],
config=data["config"],
item_ver=data["item_ver"],
session_id=data["session_id"],
finished=finished
)
serializer = ItemVersionSerializer(data=request.data)
if serializer.is_valid():
# item_version.save() # data gets saved even with this line off
return Response(serializer.validated_data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
When passing incorrect data, e.g. too short item_ver string, I receive error 400 and this info:
{
"item_ver": [
"Ensure this field has at least 6 characters."
]
}
However, when I correct the data and send it again, I receive IntegrityError: UNIQUE constraint failed: core_itemversion.item_ver_id and looking at the list of instances I can see the new entry saved to the database anyway.
Which method is responsible for saving the data?
Make sure that you validate your serializer BEFORE creating. Like this:
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
As you can see, there is no need for if else when doing serializer.is_valid(), just use raise_exception=True, and the viewset will make sure that the response will be a 400_BAD_REQUEST.
Second, don't do Model.objects.create(), then save. Do ModelSerializer(request.data), and then follow the convention.
Hopefully this makes sense to you, do let me know if you need me to expand.
I am trying to take input multiple data object in post request, but getting such error.
non_field_errors: [ Invalid data. Expected a dictionary, but got a list. ]
models.py
class OrderProduct(BaseModel):
product = models.ForeignKey(Product,on_delete=models.CASCADE)
order = models.ForeignKey(Order,on_delete=models.CASCADE)
order_product_price = models.FloatField(blank=False,null=False,default=0) # product may belong to offer do the price
order_product_qty = models.FloatField(default=1)
serializers.py
class OrderProductSerializer(serializers.ModelSerializer):
def update(self,instance,validated_data):
product = self.validated_data.pop('product')
order = self.validated_data.pop('order')
instance.orderproduct_qty =
self.validated_data.get('orderproduct_qty',instance.orderproduct_qty)
instance.product = product
instance.order = order
instance.save()
return instance
class Meta:
model = OrderProduct
fields = '__all__'
views.py
def post(self,request,*args,**kwargs):
if request.data['contact_number'] == '':
request.POST._mutable =True
request.data['contact_number'] = request.user.mobile_number
request.POST._mutable = False
serializer = OrderSerializer(data=request.data,many=isinstance(request.data,list),context={'request': request})
print(serializer)
if serializer.is_valid():
serializer.save(user = request.user,created_by = request.user)
return Response(serializer.data,status=status.HTTP_200_OK)
else:
return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)
urls.py
path('orderproduct/',views.OrderProductList.as_view()),
When you call serializer.save(). It's only perform create() action which is only create one and accept dictionary data type only. If you want to save multiple data like that, you will have to override the create function of the serializer class. You can do something similar like this or run a for loop.
serializers.py
def create(self, validate_data):
# Get the data objects you need to perform bulk create
order_products = OrderProduct.objects.bulk_create(validate_data)
return order_products
views.py
if serializer.is_valid(raise_exception=True):
# Replace the serializer.save() by this line to trigger the create method in serializer
self.perform_create(serializer)
return Response(...)
I developed APIs using Django REST Framework for an inventory management application.
The endpoint to GET the list of products includes query parameters to filter the list. See below.
Product List View:
class ProductListAPIView(ListAPIView):
serializer_class = ProductListSerializer
queryset = Product.objects.all()
permission_classes = [DjangoModelPermissionsWithView]
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = [
'sku',
'product_name',
...
]
filter_class = ProductFilter
pagination_class = ProductPageNumberPagination
ordering = ['-id']
ordering_fields = [
'id',
'sku',
'product_name',
...
]
def get_serializer_context(self, *args, **kwargs):
return {"request": self.request}
I have created another view to handle requests in order export the products to PDF, CSV, etc:
class ProductExportAPIView(APIView):
def put(self, request, *args, **kwargs):
# We use the seriaziler only to validate request.data
serializer = ProductExportSerializer(data=request.data)
if serializer.is_valid():
user_id = request.user.pk
file_key = request.data.get('file_key')
file_name = request.data.get('file_name', '')
extra_args = request.data.get('extra_args', {})
product_ids = request.data.get('product_ids')
# NOTE THAT export_file IS A CELERY TASK
export_file.delay(user_id, file_key, file_name, product_ids, extra_args)
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The API it's working fine, but it works only if the user selects the products - the product_ids field is used to provide the list of products to be exported.
I would like to let the users export ALL the products via ProductExportAPIView by providing the query params that I'm using with ProductListAPIView rather than providing product_ids.
product_ids should be an optional field to be used only to export a few products.
How I can enable query parameters filtering on my ProductExportAPIView, there is a way to do this without hardcoding it? Can I extend ProductListAPIView with the PUT method to export products?
In order to use the same query parameters defined in ProductListAPIView, now ProductExportAPIView extends ProductListAPIView, so it inherits everything I needed:
class ProductExportAPIView(ProductListAPIView):
permission_classes = [AllowAny]
http_method_names = ['put'] # disable GET method inherited from ProductListAPIView
def put(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# We use the serializer only to validate request.data
serializer = ProductExportSerializer(data=request.data)
if serializer.is_valid():
user_id = request.user.pk
file_key = request.data.get('file_key')
file_name = request.data.get('file_name', '')
extra_args = request.data.get('extra_args', {})
product_ids = request.data.get('product_ids', [])
if len(product_ids)==0:
product_ids = [p.pk for p in queryset]
export_file.delay(user_id, file_key, file_name, product_ids, extra_args)
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)