Django REST - PUT and PATCH create new object instead of updating - django-rest-framework

I'm creating a basic Django REST API that will work with a catalogue of products. I have to check every time a non-registered user GETS a product and add to the current value. And the GET and DELETE work perfectly fine. The problem is that whenever I want to update a product, a new Product is created instead.
I have 2 models:
class Brand(models.Model):
brand_name = models.CharField(max_length=500)
def __str__(self):
return self.brand_name
class Product(models.Model):
sku = models.PositiveIntegerField(primary_key=True, validators=[MaxValueValidator(999999999999999)])
product_name = models.CharField(max_length=500)
product_price = models.DecimalField( max_digits=50, decimal_places=2)
product_brand = models.ForeignKey(Brand, on_delete=models.CASCADE)
times_searched_anonymous = models.PositiveIntegerField(default=0)
def __str__ (self):
return self.product_name + ' (' + self.product_brand.brand_name + ')' + ' - $' + str(self.product_price)
These are my serializers:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = [
"sku",
"product_name",
"product_price",
"product_brand",
"times_searched_anonymous"
]
class BrandSerializer(serializers.ModelSerializer):
class Meta:
model = Brand
fields = [
"brand_name",
"pk",
]
And this is the view I have:
class ProductDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Retrieve, update or delete a product instance.
"""
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_object(self, pk):
try:
return Product.objects.get(pk=pk)
except Product.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
product = self.get_object(pk)
serializer = ProductSerializer(product)
if not request.user.is_authenticated:
data2 = {'times_searched_anonymous': product.times_searched_anonymous + 1}
serializer2 = ProductSerializer(product, data=data2, partial=True)
if serializer2.is_valid():
serializer2.save()
return Response(serializer2.data)
return Response(serializer.data)
def put(self, request, pk, format=None):
product = self.get_object(pk)
serializer = ProductSerializer(product, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, pk):
product = self.get_object(pk)
serializer = ProductSerializer(product, data=request.data,
partial=True) # set partial=True to update a data partially
if serializer.is_valid():
serializer.save()
return JsonResponse(code=201, data=serializer.data)
return JsonResponse(code=400, data="wrong parameters")
def delete(self, request, pk, format=None):
product = self.get_object(pk)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Thougths?

in put method change line
serializer = ProductSerializer(product, data=request.data, partial=True)
to be serializer = ProductSerializer(instance=product, data=request.data, partial=False)
also in patch method change line
serializer = ProductSerializer(product, data=request.data, partial=True)
to be
serializer = ProductSerializer(instance=product, data=request.data, partial=False)

Related

Permission depends on field django rest framework

I'm trying to write little project which is using django rest framework and I have the Book model and rest api for that:
#models.py
class Book(models.Model):
name = models.CharField(max_length=100)
language = models.CharField(max_length=3, choices=LANGUAGES)
publication_date = models.CharField(max_length=4)
class Meta:
verbose_name = 'Книга'
verbose_name_plural = 'Книги'
def __str__(self):
return self.name
The next serialiser:
#serializers.py
class BookSerializer(serializers.Serializer):
name = serializers.CharField(max_length=120)
language = serializers.CharField()
publication_date = serializers.CharField()
def create(self, validated_data):
return Book.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.language = validated_data.get('language', instance.language)
instance.publication_date = validated_data.get(
'publication_date',
instance.publication_date
)
instance.save()
return instance
And the next view:
#views.py
class BookView(APIView):
http_method_names = ['get', 'post', 'put', 'delete']
def get(self, request):
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
return Response({"books": serializer.data})
def post(self, request):
book = request.data.get('book')
# Create an article from the above data
serializer = BookSerializer(data=book)
if serializer.is_valid(raise_exception=True):
book_saved = serializer.save()
return Response({"success": "Book '{}' created successfully".format(book_saved.name)})
def put(self, request, pk, *args, **kwargs):
saved_book = get_object_or_404(Book.objects.all(), pk=pk)
data = request.data.get('book')
serializer = BookSerializer(instance=saved_book, data=data, partial=True)
if serializer.is_valid(raise_exception=True):
book_saved = serializer.save()
return Response({
"success": "Book '{}' updated successfully".format(book_saved.name)
})
def delete(self, request, pk, *args, **kwargs):
book = get_object_or_404(Book.objects.all(), pk=pk)
book.delete()
return Response(
{"message": "Book with id `{}` has been deleted.".format(pk)},
status=204
)
How I can to implement permission which depends on publication_date field, literally if publication_date more than current date, then get request is available only for admins.
Instead of using Book.objects.all() everywhere, define a function:
def get_queryset(self, request):
if request.user.is_superuser:
return Book.objects.all()
else:
return Book.objects.filter(publication_date__lte=datetime.datetime.now())
Then call it like self.get_queryset(request).
This will only make books published earlier than right now to be visible to non-admins.
Also, you have a lot of boilerplate code. You should probably be using ModelViewSet

How to write the functionality in generics.ListAPIView which can be written in APIView in Django DRF

I have a function base view which get 2 parameters from URL
http://127.0.0.1:8000/api/v1/contest/0b36d92a-51a7-4752-9df1-e5f2733116c1/paintings/
#api_view(['GET',])
#permission_classes([AllowAny])
def Contest_detail_by_id_and_category(request, id, category_name):
if request.method == 'GET':
artcontests = Artwork.objects.filter(artcontest = id,category__name__iexact=category_name)
serializer = ArtworkSerializer(artcontests, many=True)
# serializer = ArtworkSerializer(artcontests, many=True)
return Response(serializer.data)
which give proper result , but when I try to write the same functionality in generics.ListAPIView it gives
TypeError at /api/v1/contesty/0b36d92a-51a7-4752-9df1-e5f2733116c1/paintings/
object of type 'method' has no len()
class Contest_detail_by_id_category(generics.ListAPIView):
serializer_class = ArtworkSerializer1(many=True)
permission_classes = [AllowAny]
def queryset(self):
queryset = Artwork.objects.filter(artcontest = self.kwargs['id'],category__name__iexact=self.kwargs['category_name'])
# queryset = self.get_queryset()
serializer = ArtworkSerializer1(queryset)
return Response(serializer.data)
Can anyone help me - how to write the correct view in generics.ListAPIView or viewsets.ModelViewSet
Remove many=True from serializer_class. Create get_queryset function as below, instead of your queryset function.
class Contest_detail_by_id_category(generics.ListAPIView):
serializer_class = ArtworkSerializer1
queryset = Artwork.objects.all()
permission_classes = [AllowAny]
def get_queryset(self):
return self.queryset.filter(artcontest = self.kwargs['id'],category__name__iexact=self.kwargs['category_name'])
Update: Working:
ListAPIView class inherits ListModelMixin.
class ListModelMixin:
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
See here. https://github.com/encode/django-rest-framework/blob/master/rest_framework/mixins.py

Retrieve Single Object in Modelviewset

I was using CBV, ModelViewSet but somehow my API Keep showing all the Objects inside, how i get my ModelViewSet retrieve a single object that base on my sku ?
api/views.py
class SamAPIViewSet(viewsets.ModelViewSet):
queryset = SamModels.objects.all()
serializer_class = SamSerializers
lookup_field = 'pk'
def get_queryset(self):
return self.queryset
def get_object(self):
sku_id = self.kwargs['pk']
return self.get_queryset().filter(id=sku_id)
def list(self, request):
products = SamModels.objects.all()
serializers = self.get_serializer(products, many=True)
return Response(serializers.data)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializers = self.get_serializer(instance)
return Response(serializers.data)
api/urls.py
sam_api_list = SamAPIViewSet.as_view({
'get': 'list'
})
sam_api_detail = SamAPIViewSet.as_view({
'get': 'retrieve'
})
urlpatterns = [
url(r'sam', sam_api_list, name='api-sam'),
url(r'sam/<int:pk>', sam_api_detail, name='api-sam-detail'),
]
sam/1, it return all of my objects, i am not sure what wrong, my object has name, id, sku and how do i change my view to get retrieve single object base on my sku and not my id ?
Your routes are upside down. Try this:
urlpatterns = [
url(r'sam/<int:pk>', sam_api_detail, name='api-sam-detail'),
url(r'sam', sam_api_list, name='api-sam'),
]
Django will select the first expression that matches. If r'sam' is above r'sam/int:pk' then it will select r'sam' without going down to the next entry
url(r'sam/<int:pk>', sam_api_detail, name='api-sam-detail'),
url(r'sam', sam_api_list, name='api-sam'),
to
path(r'sam/<str:pk>', sam_api_detail, name='api-sam-detail'),
path(r'sam', sam_api_list, name='api-sam'),
remove
def get_queryset(self):
return self.queryset
def get_object(self):
sku_id = self.kwargs['pk']
return self.get_queryset().filter(id=sku_id)
and change lookup_field = 'pk' to lookup_field = 'sku'

django rest framework - override viewsets.ViewSet#retrieve in order to use 2 parameters for lookup resource

I'd like to use two model's attributes to lookup a Route's record
models.py
class Route(DateTimeModel):
start_poi = models.ForeignKey(Poi, related_name="start_pois", on_delete=models.CASCADE)
end_poi = models.ForeignKey(Poi, related_name="end_pois", on_delete=models.CASCADE)
...
def __str__(self):
return 'From %s to %s' % (self.start_poi.slug, self.end_poi.slug)
views.py
class RouteViewSet(viewsets.ViewSet):
http_method_names = ['get']
#staticmethod
def list(request):
queryset = ...
serializer = RouteSerializer(queryset, many=True)
return Response(serializer.data)
#staticmethod
def retrieve(request, from_poi_slug, to_poi_slug):
queryset = ...
route = get_object_or_404(queryset, from_poi_slug=from_poi_slug, to_poi_slug=to_poi_slug)
serializer = RouteSerializer(route)
return Response(serializer.data)
urls.py
urlpatterns.extend([
path(rf'{BASE_API_PATH}/routes/(?P<from_poi_slug>[-\w]+)/(?P<to_poi_slug>[-\w]+)', RouteViewSet),
])
I get
Not Found: /api/v1/routes/xyz/abc
[29/Apr/2019 10:07:01] "GET /api/v1/routes/molo-santa-maria/kennedy-ne HTTP/1.1" 404 13191
What am I missing?
How can I properly override #retrieve and correctly configure urls?
Since you are only using read operations, you could use ReadOnlyModelViewSet.
#views.py
class RouteViewSet(viewsets.ReadOnlyModelViewSet):
http_method_names = ['get']
def list(self, request, *args, **kwargs):
queryset = ...
serializer = RouteSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, *args, **kwargs):
from_poi_slug = kwargs['from_poi_slug']
to_poi_slug = kwargs['to_poi_slug']
queryset = ...
route = get_object_or_404(queryset, from_poi_slug=from_poi_slug, to_poi_slug=to_poi_slug)
serializer = RouteSerializer(route)
return Response(serializer.data)
and in your urls.py
urlpatterns.extend([
path(rf'{BASE_API_PATH}/routes/(<from_poi_slug>)/(<to_poi_slug>)/', RouteViewSet.as_view({"get": "retrieve"})),
])

Django REST Framework - Extending ListAPIView with custom PUT method

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)

Resources