Django REST Framework - Extending ListAPIView with custom PUT method - django-rest-framework

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)

Related

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'

Cannot generate post request for multiple data

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(...)

how to update serializer.validated_data in DRF

good day
me need update validated_data dict
example
class ProductsApiView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = ProductSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
product = serializer.save()
print(serializer.validated_data)
return Response({'message': 'created'}, status=status.HTTP_201_CREATED)
class ProductSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['my_data'] = 'my_new_data'
return super(ProductSerializer, self).create(validated_data)
class Meta:
model = Product
fields = ['id', 'title']
when i make print(serializer.validated_data)
OrderedDict([('title', 'testttt')])
why in dict no my data "my_data" ?
You can provide arbitrary additional context by passing a context argument when instantiating the serializer. For example:
serializer = AccountSerializer(account, context={'request': request})
serializer.data
The context dictionary can be used within any serializer field logic, such as a custom .to_representation() method, by accessing the self.context attribute.
Reference

django rest_framework serializer parameter

I am using django rest_framework to provide jsGrid json data.
As the rest_framwork example, I create a object fit the jsGrid format
class jsGridResp(object):
def __init__(self, data, itemsCount):
self.data = data
self.itemsCount = itemsCount
and the class based view, create a get function
class RateListViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""
API endpoint that allows user to be viewed or edited
"""
queryset = RateList.objects.all().order_by('-create_date')
serializer_class = RateListSerializer
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
obj = queryset.get(pk=self.request.POST["id"])
self.check_object_permissions(self.request, obj)
return obj
def get(self, request, format=None):
pageIndex = request.GET.get('pageIndex')
pageSize = request.GET.get('pageSize')
sortField = request.GET.get('sortField', 'id')
sortOrder = request.GET.get('sortOrder', 'asc')
sortOrder = "" if sortOrder == "asc" else "-"
rows = RateList.objects.all().order_by("{}{}".format(sortOrder, sortField))
itemsCount = rows.count()
paginator = Paginator(rows, pageSize)
try:
rows = paginator.page(pageIndex)
except PageNotAnInteger:
rows = paginator.page(1)
except EmptyPage:
rows = paginator.page(paginator.num_pages)
result = jsGridResp(data=rows, itemsCount=itemsCount)
serializer = RateListGetSerializer(result)
json = JSONRenderer().render(serializer.data)
return Response(json)
then I create two serializer to serialize the data
class RateListSerializer(serializers.ModelSerializer):
class Meta:
model = RateList
fields = ('id', 'rate_code', 'hr01', 'hr02', 'hr03', 'hr04', 'hr05', 'hr06',
'hr07', 'hr08', 'hr09', 'hr10', 'hr11', 'hr12', 'hr13', 'hr14',
'hr15', 'note', 'create_date', 'update_date')
read_only_fields = ('create_date', 'update_date')
def update(self, instance, validated_data):
result = instance.update(id=instance.id, **validated_data)
return result
def destroy(self, instace, validated_data):
return "{seccuess: true}"
class RateListGetSerializer(serializers.Serializer):
itemsCount = serializers.IntegerField()
data = RateListSerializer(many=True)
but I have many model need to do like these.
can I use just one serializer to serialize all model.
I want to create a jsGridGetSerializer can pass in a model parameter, so I don't need to create many simple serialzer to do the same thing.
Is this possible?
If I understand your problem correctly, you want a generic serializer which accepts a Model variable as a parameter in its Meta class.
One way to do this is to pass in the model name as a url keyword argument, then catching it in views.py. Then you can override the Meta model via overriding the get_serializer_class:
serializers.py
class GenericSerializer(serializers.ModelSerializer):
class Meta:
model = None
views.py
class GenericViewSet(viewsets.ModelViewSet):
def get_queryset(self):
model = self.kwargs.get('model')
return model.objects.all()
def get_serializer_class(self):
GenericSerializer.Meta.model = self.kwargs.get('model')
return GenericSerializer

Where to add default value in CreateAPIView?

I have a ListCreateAPIView, on which I want to populate a field with a default value in case of not provided by request.DATA.
Problem is: where should I do that ?
I can't modify the request.DATA because it is immutable and I don't want to copy/paste the CreateMixin implementation.
Here is my code:
class ObjectiveList(generics.ListCreateAPIView):
model = Objective
serializer_class = ObjectiveSerializer
permission_classes = (permissions.IsAuthenticated,)
def create(self, request, *args, **kwargs):
# provide a default value
objective_definition_id = request.DATA.get('objective_definition',-1)
data = request.DATA.copy()
if objective_definition_id == -1:
# support for 0.9.1 version of iOS and below
logger.info(str(self.request.DATA))
mission_url = request.DATA["mission"]
objectivedefinition_pk = self.default_objectivedefinition_id(mission_url)
data["objective_definition"]=objectivedefinition_pk
# I would want to do something like this but I can't
# request.DATA = data
# super(ObjectiveList,self).create(request, *args, **kwargs)
# copy/paste of the super class implementation
serializer = self.get_serializer(data=data, files=request.FILES)
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)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Here is my serializer:
class ObjectiveSerializer(serializers.HyperlinkedModelSerializer):
objective_definition = serializers.PrimaryKeyRelatedField(many=False, read_only=False, required=False, default=toto)
class Meta:
model = Objective
fields = (
'url',
'objective_definition',
)
You can use the default= argument on the field, don't know if that meets your use case?

Resources